redis-cluster-client 0.0.12 → 0.3.0

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: 2aec24d7a41cc222cf5fdec13b063d0ce2a68df59c05049a4719f71b6ac07391
4
- data.tar.gz: 00b0eebd7da92305fbf2512f660721ad0976cfb1e33fb72037a673fe65899ce1
3
+ metadata.gz: d9908afa1b60432aec9067825832fa16d71ce90075b70f7ecb8d4271bd021e36
4
+ data.tar.gz: 3d613f5604cef8d67fea9c7bf24b93ac1313000faaa86162dc471a00c92e3cfd
5
5
  SHA512:
6
- metadata.gz: 28d31d104bd1f2100c0f61c765783e86fea745910c748510bdc7b4985f779df4e8d8240a38afc043504cc36ad33a00273288762defcdcd03e7486cece5351882
7
- data.tar.gz: 82a6c2b2e786f27183cc000ca202e4d6bf29b6260dc00a2659eb18660e6e4b3d60946d170606938242dc9340b4ae644aaa15de7369c23e2f9cda0c2bab604fe8
6
+ metadata.gz: 13bd2cff41f0dfc139d1618c8eb402ee1883b20ccf5fbcc78c79ff0f6ddd740860db138959f437c02b238764eec884cc720fb4093b339a7a31e472c2cdf044c9
7
+ data.tar.gz: a5ba95acecca5f555589bca85d6c1addf3585172387a1a95afde6d628fd5da0d24536506aa86eaaaf368606f71a28662b3c4999fe7d8f522f4ae21d75aaf1759
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_cluster_client'
@@ -55,6 +55,10 @@ class RedisClient
55
55
  dig_details(command, :readonly)
56
56
  end
57
57
 
58
+ def exists?(name)
59
+ @details.key?(name.to_s.downcase)
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def pick_details(details)
@@ -0,0 +1,82 @@
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_SEC = 100.0
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
+ min = DUMMY_LATENCY_SEC + 1.0
49
+ MEASURE_ATTEMPT_COUNT.times do
50
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC)
51
+ client.send(:call_once, 'PING')
52
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - starting
53
+ min = duration if duration < min
54
+ end
55
+ latencies[node_key] = min
56
+ rescue StandardError
57
+ latencies[node_key] = DUMMY_LATENCY_SEC
58
+ end
59
+ end
60
+
61
+ threads.each(&:join)
62
+ end
63
+
64
+ latencies
65
+ end
66
+
67
+ def select_replica_clients(replications, clients)
68
+ keys = replications.values.filter_map(&:first)
69
+ clients.select { |k, _| keys.include?(k) }
70
+ end
71
+
72
+ def select_clients_for_scanning(replications, clients)
73
+ keys = replications.map do |primary_node_key, replica_node_keys|
74
+ replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
75
+ end
76
+
77
+ clients.select { |k, _| keys.include?(k) }
78
+ end
79
+ end
80
+ end
81
+ end
82
+ 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)
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)
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)
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).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
@@ -21,37 +21,36 @@ class RedisClient
21
21
  @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
22
22
  @command = ::RedisClient::Cluster::Command.load(@node)
23
23
  @mutex = Mutex.new
24
+ @command_builder = @config.command_builder
24
25
  end
25
26
 
26
- def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
27
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
28
-
27
+ def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
29
28
  cmd = command.first.to_s.downcase
30
29
  case cmd
31
30
  when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
32
- @node.call_all(method, *args, **kwargs, &block).first
31
+ @node.call_all(method, command, args, &block).first
33
32
  when 'flushall', 'flushdb'
34
- @node.call_primaries(method, *args, **kwargs, &block).first
35
- when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
36
- when 'wait' then send_wait_command(method, *args, **kwargs, &block)
37
- when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort_by(&:to_s)
38
- when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
39
- when 'scan' then scan(*command, **kwargs)
40
- when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort_by(&:to_i)
41
- when 'role' then @node.call_all(method, *args, **kwargs, &block)
42
- when 'config' then send_config_command(method, *args, **kwargs, &block)
43
- when 'client' then send_client_command(method, *args, **kwargs, &block)
44
- 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)
45
44
  when 'readonly', 'readwrite', 'shutdown'
46
45
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
47
- when 'memory' then send_memory_command(method, *args, **kwargs, &block)
48
- when 'script' then send_script_command(method, *args, **kwargs, &block)
49
- 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)
50
49
  when 'discard', 'exec', 'multi', 'unwatch'
51
50
  raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
52
51
  else
53
- node = assign_node(*command)
54
- try_send(node, method, *args, **kwargs, &block)
52
+ node = assign_node(command)
53
+ try_send(node, method, command, args, &block)
55
54
  end
56
55
  rescue ::RedisClient::Cluster::Node::ReloadNeeded
57
56
  update_cluster_info!
@@ -65,7 +64,37 @@ class RedisClient
65
64
 
66
65
  # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
67
66
  # Redirection and resharding
68
- 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
69
98
  node.send(method, *args, **kwargs, &block)
70
99
  rescue ::RedisClient::CommandError => e
71
100
  raise if retry_count <= 0
@@ -94,21 +123,23 @@ class RedisClient
94
123
  retry
95
124
  end
96
125
 
97
- def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
126
+ def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
127
+ command = @command_builder.generate(command, kwargs)
128
+
98
129
  command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
99
130
  input_cursor = Integer(command[1])
100
131
 
101
132
  client_index = input_cursor % 256
102
133
  raw_cursor = input_cursor >> 8
103
134
 
104
- clients = @node.scale_reading_clients
135
+ clients = @node.clients_for_scanning(seed: seed)
105
136
 
106
137
  client = clients[client_index]
107
138
  return [ZERO_CURSOR_FOR_SCAN, []] unless client
108
139
 
109
140
  command[1] = raw_cursor.to_s
110
141
 
111
- result_cursor, result_keys = client.call(*command, **kwargs)
142
+ result_cursor, result_keys = client.call_v(command)
112
143
  result_cursor = Integer(result_cursor)
113
144
 
114
145
  client_index += 1 if result_cursor == 0
@@ -116,19 +147,19 @@ class RedisClient
116
147
  [((result_cursor << 8) + client_index).to_s, result_keys]
117
148
  end
118
149
 
119
- def assign_node(*command)
120
- node_key = find_node_key(*command)
150
+ def assign_node(command)
151
+ node_key = find_node_key(command)
121
152
  find_node(node_key)
122
153
  end
123
154
 
124
- def find_node_key(*command, primary_only: false)
155
+ def find_node_key(command, seed: nil)
125
156
  key = @command.extract_first_key(command)
126
157
  slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
127
158
 
128
- if @command.should_send_to_primary?(command) || primary_only
129
- @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)
130
161
  else
131
- @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)
132
163
  end
133
164
  end
134
165
 
@@ -142,10 +173,14 @@ class RedisClient
142
173
  retry
143
174
  end
144
175
 
176
+ def command_exists?(name)
177
+ @command.exists?(name)
178
+ end
179
+
145
180
  private
146
181
 
147
- def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
148
- @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
149
184
  rescue ::RedisClient::Cluster::ErrorCollection => e
150
185
  raise if retry_count <= 0
151
186
  raise if e.errors.values.none? do |err|
@@ -157,76 +192,65 @@ class RedisClient
157
192
  retry
158
193
  end
159
194
 
160
- def send_config_command(method, *args, **kwargs, &block)
161
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
162
-
195
+ def send_config_command(method, command, args, &block)
163
196
  case command[1].to_s.downcase
164
197
  when 'resetstat', 'rewrite', 'set'
165
- @node.call_all(method, *args, **kwargs, &block).first
166
- 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)
167
200
  end
168
201
  end
169
202
 
170
- def send_memory_command(method, *args, **kwargs, &block)
171
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
172
-
203
+ def send_memory_command(method, command, args, &block)
173
204
  case command[1].to_s.downcase
174
- when 'stats' then @node.call_all(method, *args, **kwargs, &block)
175
- when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
176
- 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)
177
208
  end
178
209
  end
179
210
 
180
- def send_client_command(method, *args, **kwargs, &block)
181
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
182
-
211
+ def send_client_command(method, command, args, &block)
183
212
  case command[1].to_s.downcase
184
- when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
213
+ when 'list' then @node.call_all(method, command, args, &block).flatten
185
214
  when 'pause', 'reply', 'setname'
186
- @node.call_all(method, *args, **kwargs, &block).first
187
- 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)
188
217
  end
189
218
  end
190
219
 
191
- def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
192
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
220
+ def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/MethodLength
193
221
  subcommand = command[1].to_s.downcase
194
222
 
195
223
  case subcommand
196
224
  when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
197
225
  'reset', 'set-config-epoch', 'setslot'
198
226
  raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
199
- when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
227
+ when 'saveconfig' then @node.call_all(method, command, args, &block).first
200
228
  when 'getkeysinslot'
201
229
  raise ArgumentError, command.join(' ') if command.size != 4
202
230
 
203
- find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
204
- 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)
205
233
  end
206
234
  end
207
235
 
208
- def send_script_command(method, *args, **kwargs, &block)
209
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
210
-
236
+ def send_script_command(method, command, args, &block)
211
237
  case command[1].to_s.downcase
212
238
  when 'debug', 'kill'
213
- @node.call_all(method, *args, **kwargs, &block).first
239
+ @node.call_all(method, command, args, &block).first
214
240
  when 'flush', 'load'
215
- @node.call_primaries(method, *args, **kwargs, &block).first
216
- 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)
217
243
  end
218
244
  end
219
245
 
220
- def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
221
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
222
-
246
+ def send_pubsub_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
223
247
  case command[1].to_s.downcase
224
- 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)
225
249
  when 'numsub'
226
- @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] }
227
251
  .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
228
- when 'numpat' then @node.call_all(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
229
- 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)
230
254
  end
231
255
  end
232
256
 
@@ -242,18 +266,24 @@ class RedisClient
242
266
  find_node(node_key)
243
267
  end
244
268
 
245
- def fetch_cluster_info(config, pool: nil, **kwargs)
269
+ def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
246
270
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
247
271
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
248
272
  config.update_node(node_addrs)
249
- ::RedisClient::Cluster::Node.new(config.per_node_key,
250
- 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
+ )
251
281
  end
252
282
 
253
283
  def update_cluster_info!
254
284
  @mutex.synchronize do
255
285
  begin
256
- @node.call_all(:close)
286
+ @node.each(&:close)
257
287
  rescue ::RedisClient::Cluster::ErrorCollection
258
288
  # ignore
259
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)
21
- @router.send_command(:call, *command, **kwargs)
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)
25
- @router.send_command(:call_once, *command, **kwargs)
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)
29
- @router.send_command(:blocking_call, timeout, *command, **kwargs)
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,12 +86,30 @@ 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
96
+
97
+ private
98
+
99
+ def method_missing(name, *args, **kwargs, &block)
100
+ if @router.command_exists?(name)
101
+ args.unshift(name)
102
+ command = @command_builder.generate(args, kwargs)
103
+ return @router.send_command(:call_v, command, &block)
104
+ end
105
+
106
+ super
107
+ end
108
+
109
+ def respond_to_missing?(name, include_private = false)
110
+ return true if @router.command_exists?(name)
111
+
112
+ super
113
+ end
74
114
  end
75
115
  end
@@ -4,6 +4,7 @@ require 'uri'
4
4
  require 'redis_client'
5
5
  require 'redis_client/cluster'
6
6
  require 'redis_client/cluster/node_key'
7
+ require 'redis_client/command_builder'
7
8
 
8
9
  class RedisClient
9
10
  class ClusterConfig
@@ -19,12 +20,25 @@ class RedisClient
19
20
 
20
21
  InvalidClientConfigError = Class.new(::RedisClient::Error)
21
22
 
22
- def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
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
+ )
33
+
23
34
  @replica = true & replica
35
+ @replica_affinity = replica_affinity.to_s.to_sym
24
36
  @fixed_hostname = fixed_hostname.to_s
25
37
  @node_configs = build_node_configs(nodes.dup)
26
38
  client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
39
+ @command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
27
40
  @client_config = merge_generic_config(client_config, @node_configs)
41
+ @client_implementation = client_implementation
28
42
  @mutex = Mutex.new
29
43
  end
30
44
 
@@ -32,12 +46,16 @@ class RedisClient
32
46
  "#<#{self.class.name} #{per_node_key.values}>"
33
47
  end
34
48
 
49
+ def read_timeout
50
+ @client_config[:read_timeout] || @client_config[:timeout] || RedisClient::Config::DEFAULT_TIMEOUT
51
+ end
52
+
35
53
  def new_pool(size: 5, timeout: 5, **kwargs)
36
- ::RedisClient::Cluster.new(self, pool: { size: size, timeout: timeout }, **kwargs)
54
+ @client_implementation.new(self, pool: { size: size, timeout: timeout }, **kwargs)
37
55
  end
38
56
 
39
57
  def new_client(**kwargs)
40
- ::RedisClient::Cluster.new(self, **kwargs)
58
+ @client_implementation.new(self, **kwargs)
41
59
  end
42
60
 
43
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.0.12
4
+ version: 0.3.0
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