redis-cluster-client 0.0.12 → 0.3.0

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: 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