redis-cluster-client 0.2.0 → 0.3.2

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: 5160c83d23cb638fc76cf079377981d6778560ba0eb476da03f58d6f4eded9af
4
- data.tar.gz: dd0baec0d24de14e2cba10c4ea3f45af7f1bef2db30f609c9551ed2d13c0bf4a
3
+ metadata.gz: 7fa6c9386e1ffd7571fdc8a29bea08960387d958149b972e1cf27d28eac98da0
4
+ data.tar.gz: e532422c94e7416df967f772112c77861f4501d5dcd73c2d9893237586077677
5
5
  SHA512:
6
- metadata.gz: 1cf5c16776bb7ff4c53ffa66e3de3371c1130fed7a4916982e7fba9859ba9ae7505af0bc5e8e02b58f22d116ab74692a4572d835f05672457bac4ddd469e3162
7
- data.tar.gz: f08215c00f2b12b00247604917a7f7abc228ddd0e4305c67af38f8e2087a8b3785a22bfbd09ac32f75789719a624be49663f5048f6187318477069e4ef9627a1
6
+ metadata.gz: ef29a0346608779f6528559e52e348df690baf44db0cd8c9e1fe94e44bf9d282eca81101a1246cb0bff418ec398b2b207eb47563e2b976b6523e0fe6583e55fb
7
+ data.tar.gz: 7e4059d96f02c078989c7bae6cc4790f1580d0fc721c545a86d694ddae7eb90541573c67bb66a93bc6732b45b1f1ac18a3370b0d3d20df41760be9eb78255f5c
@@ -0,0 +1,85 @@
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
+ @existed_replicas = @replications.reject { |_, v| v.empty? }.values
25
+ end
26
+
27
+ def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
28
+ @clients_for_scanning
29
+ end
30
+
31
+ def find_node_key_of_replica(primary_node_key, seed: nil) # rubocop:disable Lint/UnusedMethodArgument
32
+ @replications.fetch(primary_node_key, EMPTY_ARRAY).first || primary_node_key
33
+ end
34
+
35
+ def any_replica_node_key(seed: nil)
36
+ random = seed.nil? ? Random : Random.new(seed)
37
+ @existed_replicas.sample(random: random).first
38
+ end
39
+
40
+ private
41
+
42
+ def measure_latencies(clients) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
43
+ latencies = {}
44
+
45
+ clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS) do |chuncked_clients|
46
+ threads = chuncked_clients.map do |k, v|
47
+ Thread.new(k, v) do |node_key, client|
48
+ Thread.pass
49
+
50
+ min = DUMMY_LATENCY_NSEC
51
+ MEASURE_ATTEMPT_COUNT.times do
52
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
53
+ client.send(:call_once, 'PING')
54
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) - starting
55
+ min = duration if duration < min
56
+ end
57
+
58
+ latencies[node_key] = min
59
+ rescue StandardError
60
+ latencies[node_key] = DUMMY_LATENCY_NSEC
61
+ end
62
+ end
63
+
64
+ threads.each(&:join)
65
+ end
66
+
67
+ latencies
68
+ end
69
+
70
+ def select_replica_clients(replications, clients)
71
+ keys = replications.values.filter_map(&:first)
72
+ clients.select { |k, _| keys.include?(k) }
73
+ end
74
+
75
+ def select_clients_for_scanning(replications, clients)
76
+ keys = replications.map do |primary_node_key, replica_node_keys|
77
+ replica_node_keys.empty? ? primary_node_key : replica_node_keys.first
78
+ end
79
+
80
+ clients.select { |k, _| keys.include?(k) }
81
+ end
82
+ end
83
+ end
84
+ end
85
+ 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
136
  def call_all(method, command, args, &block)
132
- results, errors = try_map do |_, client|
133
- client.send(method, *args, command, &block)
134
- end
135
-
136
- return results.values if errors.empty?
137
-
138
- raise ::RedisClient::Cluster::ErrorCollection, errors
137
+ call_multiple_nodes!(@topology.clients, method, command, args, &block)
139
138
  end
140
139
 
141
140
  def call_primaries(method, command, args, &block)
142
- results, errors = try_map do |node_key, client|
143
- next if replica?(node_key)
144
-
145
- client.send(method, *args, command, &block)
146
- end
147
-
148
- return results.values if errors.empty?
149
-
150
- raise ::RedisClient::Cluster::ErrorCollection, errors
141
+ call_multiple_nodes!(@topology.primary_clients, method, command, args, &block)
151
142
  end
152
143
 
153
144
  def call_replicas(method, command, args, &block)
154
- return call_primaries(method, command, args, &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, command, &block)
161
- end
162
-
163
- return results.values if errors.empty?
164
-
165
- raise ::RedisClient::Cluster::ErrorCollection, errors
145
+ call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
166
146
  end
167
147
 
168
148
  def send_ping(method, command, args, &block)
169
- results, errors = try_map do |_, client|
170
- client.send(method, *args, command, &block)
171
- end
172
-
173
- return results.values if errors.empty?
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,52 +7,54 @@ 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
12
  def initialize(router, command_builder)
12
13
  @router = router
13
14
  @command_builder = command_builder
14
15
  @grouped = Hash.new([].freeze)
15
16
  @size = 0
17
+ @seed = Random.new_seed
16
18
  end
17
19
 
18
20
  def call(*args, **kwargs, &block)
19
21
  command = @command_builder.generate(args, kwargs)
20
- node_key = @router.find_node_key(command, primary_only: true)
22
+ node_key = @router.find_node_key(command, seed: @seed)
21
23
  @grouped[node_key] += [[@size, :call_v, command, block]]
22
24
  @size += 1
23
25
  end
24
26
 
25
27
  def call_v(args, &block)
26
28
  command = @command_builder.generate(args)
27
- node_key = @router.find_node_key(command, primary_only: true)
29
+ node_key = @router.find_node_key(command, seed: @seed)
28
30
  @grouped[node_key] += [[@size, :call_v, command, block]]
29
31
  @size += 1
30
32
  end
31
33
 
32
34
  def call_once(*args, **kwargs, &block)
33
35
  command = @command_builder.generate(args, kwargs)
34
- node_key = @router.find_node_key(command, primary_only: true)
36
+ node_key = @router.find_node_key(command, seed: @seed)
35
37
  @grouped[node_key] += [[@size, :call_once_v, command, block]]
36
38
  @size += 1
37
39
  end
38
40
 
39
41
  def call_once_v(args, &block)
40
42
  command = @command_builder.generate(args)
41
- node_key = @router.find_node_key(command, primary_only: true)
43
+ node_key = @router.find_node_key(command, seed: @seed)
42
44
  @grouped[node_key] += [[@size, :call_once_v, command, block]]
43
45
  @size += 1
44
46
  end
45
47
 
46
48
  def blocking_call(timeout, *args, **kwargs, &block)
47
49
  command = @command_builder.generate(args, kwargs)
48
- node_key = @router.find_node_key(command, primary_only: true)
50
+ node_key = @router.find_node_key(command, seed: @seed)
49
51
  @grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
50
52
  @size += 1
51
53
  end
52
54
 
53
55
  def blocking_call_v(timeout, args, &block)
54
56
  command = @command_builder.generate(args)
55
- node_key = @router.find_node_key(command, primary_only: true)
57
+ node_key = @router.find_node_key(command, seed: @seed)
56
58
  @grouped[node_key] += [[@size, :blocking_call_v, timeout, command, block]]
57
59
  @size += 1
58
60
  end
@@ -62,27 +64,30 @@ class RedisClient
62
64
  end
63
65
 
64
66
  # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
65
- def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
67
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
66
68
  all_replies = Array.new(@size)
67
69
  errors = {}
68
- threads = @grouped.map do |k, v|
69
- Thread.new(@router, k, v) do |router, node_key, rows|
70
- Thread.pass
71
- replies = router.find_node(node_key).pipelined do |pipeline|
72
- rows.each do |(_size, *row, block)|
73
- pipeline.send(*row, &block)
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)
77
+ end
74
78
  end
75
- end
76
79
 
77
- 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
78
81
 
79
- rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
80
- rescue StandardError => e
81
- 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
82
86
  end
87
+
88
+ threads.each(&:join)
83
89
  end
84
90
 
85
- threads.each(&:join)
86
91
  return all_replies if errors.empty?
87
92
 
88
93
  raise ::RedisClient::Cluster::ErrorCollection, errors
@@ -35,7 +35,7 @@ class RedisClient
35
35
  when 'wait' then send_wait_command(method, command, args, &block)
36
36
  when 'keys' then @node.call_replicas(method, command, args, &block).flatten.sort_by(&:to_s)
37
37
  when 'dbsize' then @node.call_replicas(method, command, args, &block).select { |e| e.is_a?(Integer) }.sum
38
- when 'scan' then scan(command)
38
+ when 'scan' then scan(command, seed: 1)
39
39
  when 'lastsave' then @node.call_all(method, command, args, &block).sort_by(&:to_i)
40
40
  when 'role' then @node.call_all(method, command, args, &block)
41
41
  when 'config' then send_config_command(method, command, args, &block)
@@ -123,7 +123,7 @@ class RedisClient
123
123
  retry
124
124
  end
125
125
 
126
- def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
126
+ def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
127
127
  command = @command_builder.generate(command, kwargs)
128
128
 
129
129
  command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
@@ -132,7 +132,7 @@ class RedisClient
132
132
  client_index = input_cursor % 256
133
133
  raw_cursor = input_cursor >> 8
134
134
 
135
- clients = @node.scale_reading_clients
135
+ clients = @node.clients_for_scanning(seed: seed)
136
136
 
137
137
  client = clients[client_index]
138
138
  return [ZERO_CURSOR_FOR_SCAN, []] unless client
@@ -147,19 +147,19 @@ class RedisClient
147
147
  [((result_cursor << 8) + client_index).to_s, result_keys]
148
148
  end
149
149
 
150
- def assign_node(command, primary_only: false)
151
- node_key = find_node_key(command, primary_only: primary_only)
150
+ def assign_node(command)
151
+ node_key = find_node_key(command)
152
152
  find_node(node_key)
153
153
  end
154
154
 
155
- def find_node_key(command, primary_only: false)
155
+ def find_node_key(command, seed: nil)
156
156
  key = @command.extract_first_key(command)
157
157
  slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
158
158
 
159
- if @command.should_send_to_primary?(command) || primary_only
160
- @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)
161
161
  else
162
- @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)
163
163
  end
164
164
  end
165
165
 
@@ -266,12 +266,18 @@ class RedisClient
266
266
  find_node(node_key)
267
267
  end
268
268
 
269
- def fetch_cluster_info(config, pool: nil, **kwargs)
269
+ def fetch_cluster_info(config, pool: nil, **kwargs) # rubocop:disable Metrics/MethodLength
270
270
  node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
271
271
  node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
272
272
  config.update_node(node_addrs)
273
- ::RedisClient::Cluster::Node.new(config.per_node_key,
274
- 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
+ )
275
281
  end
276
282
 
277
283
  def update_cluster_info!
@@ -53,9 +53,10 @@ class RedisClient
53
53
  def scan(*args, **kwargs, &block)
54
54
  raise ArgumentError, 'block required' unless block
55
55
 
56
+ seed = Random.new_seed
56
57
  cursor = ZERO_CURSOR_FOR_SCAN
57
58
  loop do
58
- cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
59
+ cursor, keys = @router.scan('SCAN', cursor, *args, seed: seed, **kwargs)
59
60
  keys.each(&block)
60
61
  break if cursor == ZERO_CURSOR_FOR_SCAN
61
62
  end
@@ -20,10 +20,19 @@ class RedisClient
20
20
 
21
21
  InvalidClientConfigError = Class.new(::RedisClient::Error)
22
22
 
23
- attr_reader :command_builder, :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
+ )
24
33
 
25
- def initialize(nodes: DEFAULT_NODES, replica: false, client_implementation: Cluster, 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) }
@@ -33,6 +42,17 @@ class RedisClient
33
42
  @mutex = Mutex.new
34
43
  end
35
44
 
45
+ def dup
46
+ self.class.new(
47
+ nodes: @node_configs,
48
+ replica: @replica,
49
+ replica_affinity: @replica_affinity,
50
+ fixed_hostname: @fixed_hostname,
51
+ client_implementation: @client_implementation,
52
+ **@client_config
53
+ )
54
+ end
55
+
36
56
  def inspect
37
57
  "#<#{self.class.name} #{per_node_key.values}>"
38
58
  end
@@ -70,10 +90,6 @@ class RedisClient
70
90
  @mutex.synchronize { @node_configs << { host: host, port: port } }
71
91
  end
72
92
 
73
- def dup
74
- self.class.new(nodes: @node_configs, replica: @replica, fixed_hostname: @fixed_hostname, **@client_config)
75
- end
76
-
77
93
  private
78
94
 
79
95
  def build_node_configs(addrs)
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.2.0
4
+ version: 0.3.2
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-08-17 00:00:00.000000000 Z
11
+ date: 2022-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -37,6 +37,10 @@ files:
37
37
  - lib/redis_client/cluster/errors.rb
38
38
  - lib/redis_client/cluster/key_slot_converter.rb
39
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
40
44
  - lib/redis_client/cluster/node_key.rb
41
45
  - lib/redis_client/cluster/pipeline.rb
42
46
  - lib/redis_client/cluster/pub_sub.rb