redis-cluster-client 0.4.16 → 0.5.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: d0718a12555e9116b8fe96bc0dba71151288f824ce878c0d17d6d70b653f4a61
4
- data.tar.gz: 99decc7aaef7dedf30a16ab35bd652be4114d31e6f52ba03637c99c28c9c7e86
3
+ metadata.gz: ac7d6ca1474a42ad2d4027de72dbd087f6d248b3678ad2e360ed55b56dc60790
4
+ data.tar.gz: ce4de83f3f601772df8c95381a65172245967bd9568ff7959aec5858e55fb7af
5
5
  SHA512:
6
- metadata.gz: 23b095c4d5ca638ab8de8fbaf51b97e845ac2e0f778bb70139cbe79dc62bd93fdf75f1d5701f5e6b85d639099df9a9ad5707560094cd977b591df5d997a80964
7
- data.tar.gz: 2cb340116191ed21f4b048ee3bad5a473755e498278d4bff76a991e0b18d9b3b359ba207c3c1ec039d211a1b2cdc672d001aaf83a5079b2a2e6350c52cc6a6ae
6
+ metadata.gz: 3cb2167fb97c6ab7ccba66e888521a1a119e2f17a0c373829bce0abb41518ead4005c0a7c176373c45942e9cd9fca71355b9898e7cc2b67db22d6291a76b324e
7
+ data.tar.gz: 1da6192fcb6f33359b508ff4e3445ec029373ff9606f78170051a682a29371524d6f08864f4a0887b3cbc298e462d13fe8cc9080eb29bd9cf8d965c7a766409e
@@ -39,30 +39,27 @@ class RedisClient
39
39
 
40
40
  private
41
41
 
42
- def measure_latencies(clients) # rubocop:disable Metrics/AbcSize
42
+ def measure_latencies(clients)
43
43
  clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS).each_with_object({}) do |chuncked_clients, acc|
44
- threads = chuncked_clients.map do |k, v|
45
- Thread.new(k, v) do |node_key, client|
46
- Thread.current[:node_key] = node_key
47
-
48
- min = DUMMY_LATENCY_MSEC
49
- MEASURE_ATTEMPT_COUNT.times do
50
- starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
51
- client.call_once('PING')
52
- duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - starting
53
- min = duration if duration < min
54
- end
55
-
56
- Thread.current[:latency] = min
57
- rescue StandardError
58
- Thread.current[:latency] = DUMMY_LATENCY_MSEC
59
- end
60
- end
44
+ chuncked_clients
45
+ .map { |node_key, client| [node_key, build_thread_for_measuring_latency(client)] }
46
+ .each { |node_key, thread| acc[node_key] = thread.value }
47
+ end
48
+ end
61
49
 
62
- threads.each do |t|
63
- t.join
64
- acc[t[:node_key]] = t[:latency]
50
+ def build_thread_for_measuring_latency(client)
51
+ Thread.new(client) do |cli|
52
+ min = DUMMY_LATENCY_MSEC
53
+ MEASURE_ATTEMPT_COUNT.times do
54
+ starting = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
55
+ cli.call_once('PING')
56
+ duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond) - starting
57
+ min = duration if duration < min
65
58
  end
59
+
60
+ min
61
+ rescue StandardError
62
+ DUMMY_LATENCY_MSEC
66
63
  end
67
64
  end
68
65
 
@@ -0,0 +1,55 @@
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 RandomReplicaOrPrimary
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
+ decide_use_primary?(random, replica_node_keys.size) ? 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
+
28
+ replica_node_keys = @replications.fetch(primary_node_key, EMPTY_ARRAY)
29
+ if decide_use_primary?(random, replica_node_keys.size)
30
+ primary_node_key
31
+ else
32
+ replica_node_keys.sample(random: random) || primary_node_key
33
+ end
34
+ end
35
+
36
+ def any_replica_node_key(seed: nil)
37
+ random = seed.nil? ? Random : Random.new(seed)
38
+ @replica_node_keys.sample(random: random) || any_primary_node_key(seed: seed)
39
+ end
40
+
41
+ private
42
+
43
+ # Randomly equally likely choose node to read between primary and all replicas
44
+ # e.g. 1 primary + 1 replica = 50% probability to read from primary
45
+ # e.g. 1 primary + 2 replica = 33% probability to read from primary
46
+ # e.g. 1 primary + 0 replica = 100% probability to read from primary
47
+ def decide_use_primary?(random, replica_nodes)
48
+ primary_nodes = 1.0
49
+ total = primary_nodes + replica_nodes
50
+ random.rand < primary_nodes / total
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -24,12 +24,12 @@ class RedisClient
24
24
  private
25
25
 
26
26
  def build_clients(primary_node_keys, options, pool, **kwargs)
27
- options.filter_map do |node_key, option|
27
+ options.to_h do |node_key, option|
28
28
  option = option.merge(kwargs.reject { |k, _| ::RedisClient::Cluster::Node::IGNORE_GENERIC_CONFIG_KEYS.include?(k) })
29
29
  config = ::RedisClient::Cluster::Node::Config.new(scale_read: !primary_node_keys.include?(node_key), **option)
30
30
  client = pool.nil? ? config.new_client : config.new_pool(**pool)
31
31
  [node_key, client]
32
- end.to_h
32
+ end
33
33
  end
34
34
  end
35
35
  end
@@ -5,6 +5,7 @@ require 'redis_client/config'
5
5
  require 'redis_client/cluster/errors'
6
6
  require 'redis_client/cluster/node/primary_only'
7
7
  require 'redis_client/cluster/node/random_replica'
8
+ require 'redis_client/cluster/node/random_replica_or_primary'
8
9
  require 'redis_client/cluster/node/latency_replica'
9
10
 
10
11
  class RedisClient
@@ -94,28 +95,19 @@ class RedisClient
94
95
  startup_options = options.to_a.sample(MAX_STARTUP_SAMPLE).to_h
95
96
  startup_nodes = ::RedisClient::Cluster::Node.new(startup_options, **kwargs)
96
97
  startup_nodes.each_slice(MAX_THREADS).with_index do |chuncked_startup_nodes, chuncked_idx|
97
- threads = chuncked_startup_nodes.each_with_index.map do |raw_client, idx|
98
- Thread.new(raw_client, (MAX_THREADS * chuncked_idx) + idx) do |cli, i|
99
- Thread.current[:index] = i
100
- reply = cli.call('CLUSTER', 'NODES')
101
- Thread.current[:info] = parse_cluster_node_reply(reply)
102
- rescue StandardError => e
103
- Thread.current[:error] = e
104
- ensure
105
- cli&.close
98
+ chuncked_startup_nodes
99
+ .each_with_index
100
+ .map { |raw_client, idx| [(MAX_THREADS * chuncked_idx) + idx, build_thread_for_cluster_node(raw_client)] }
101
+ .each do |i, t|
102
+ case v = t.value
103
+ when StandardError
104
+ errors ||= Array.new(startup_size)
105
+ errors[i] = v
106
+ else
107
+ node_info_list ||= Array.new(startup_size)
108
+ node_info_list[i] = v
109
+ end
106
110
  end
107
- end
108
-
109
- threads.each do |t|
110
- t.join
111
- if t.key?(:info)
112
- node_info_list ||= Array.new(startup_size)
113
- node_info_list[t[:index]] = t[:info]
114
- elsif t.key?(:error)
115
- errors ||= Array.new(startup_size)
116
- errors[t[:index]] = t[:error]
117
- end
118
- end
119
111
  end
120
112
 
121
113
  raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
@@ -132,6 +124,17 @@ class RedisClient
132
124
 
133
125
  private
134
126
 
127
+ def build_thread_for_cluster_node(raw_client)
128
+ Thread.new(raw_client) do |client|
129
+ reply = client.call('CLUSTER', 'NODES')
130
+ parse_cluster_node_reply(reply)
131
+ rescue StandardError => e
132
+ e
133
+ ensure
134
+ client&.close
135
+ end
136
+ end
137
+
135
138
  # @see https://redis.io/commands/cluster-nodes/
136
139
  # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
137
140
  def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
@@ -282,6 +285,8 @@ class RedisClient
282
285
  def make_topology_class(with_replica, replica_affinity)
283
286
  if with_replica && replica_affinity == :random
284
287
  ::RedisClient::Cluster::Node::RandomReplica
288
+ elsif with_replica && replica_affinity == :random_with_primary
289
+ ::RedisClient::Cluster::Node::RandomReplicaOrPrimary
285
290
  elsif with_replica && replica_affinity == :latency
286
291
  ::RedisClient::Cluster::Node::LatencyReplica
287
292
  else
@@ -331,33 +336,33 @@ class RedisClient
331
336
  raise ::RedisClient::Cluster::ErrorCollection, errors
332
337
  end
333
338
 
334
- def try_map(clients) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
339
+ def try_map(clients, &block)
335
340
  results = errors = nil
336
341
  clients.each_slice(MAX_THREADS) do |chuncked_clients|
337
- threads = chuncked_clients.map do |k, v|
338
- Thread.new(k, v) do |node_key, client|
339
- Thread.current[:node_key] = node_key
340
- reply = yield(node_key, client)
341
- Thread.current[:result] = reply
342
- rescue StandardError => e
343
- Thread.current[:error] = e
344
- end
345
- end
346
-
347
- threads.each do |t|
348
- t.join
349
- if t.key?(:result)
350
- results ||= {}
351
- results[t[:node_key]] = t[:result]
352
- elsif t.key?(:error)
353
- errors ||= {}
354
- errors[t[:node_key]] = t[:error]
342
+ chuncked_clients
343
+ .map { |node_key, client| [node_key, build_thread_for_command(node_key, client, &block)] }
344
+ .each do |node_key, thread|
345
+ case v = thread.value
346
+ when StandardError
347
+ errors ||= {}
348
+ errors[node_key] = v
349
+ else
350
+ results ||= {}
351
+ results[node_key] = v
352
+ end
355
353
  end
356
- end
357
354
  end
358
355
 
359
356
  [results, errors]
360
357
  end
358
+
359
+ def build_thread_for_command(node_key, client)
360
+ Thread.new(node_key, client) do |nk, cli|
361
+ yield(nk, cli)
362
+ rescue StandardError => e
363
+ e
364
+ end
365
+ end
361
366
  end
362
367
  end
363
368
  end
@@ -148,38 +148,23 @@ class RedisClient
148
148
  def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
149
149
  all_replies = errors = nil
150
150
  @pipelines&.each_slice(MAX_THREADS) do |chuncked_pipelines|
151
- threads = chuncked_pipelines.map do |node_key, pipeline|
152
- Thread.new(node_key, pipeline) do |nk, pl|
153
- Thread.current[:node_key] = nk
154
- replies = do_pipelining(@router.find_node(nk), pl)
155
- raise ReplySizeError, "commands: #{pl._size}, replies: #{replies.size}" if pl._size != replies.size
156
-
157
- Thread.current[:replies] = replies
158
- rescue ::RedisClient::Cluster::Pipeline::RedirectionNeeded => e
159
- Thread.current[:redirection_needed] = e
160
- rescue StandardError => e
161
- Thread.current[:error] = e
162
- end
163
- end
164
-
165
- threads.each(&:join)
166
- threads.each do |t|
167
- if t.key?(:replies)
168
- all_replies ||= Array.new(@size)
169
- @pipelines[t[:node_key]]
170
- .outer_indices
171
- .each_with_index { |outer, inner| all_replies[outer] = t[:replies][inner] }
172
- elsif t.key?(:redirection_needed)
173
- all_replies ||= Array.new(@size)
174
- pipeline = @pipelines[t[:node_key]]
175
- err = t[:redirection_needed]
176
- err.indices.each { |i| err.replies[i] = handle_redirection(err.replies[i], pipeline, i) }
177
- pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = err.replies[inner] }
178
- elsif t.key?(:error)
179
- errors ||= {}
180
- errors[t[:node_key]] = t[:error]
151
+ chuncked_pipelines
152
+ .map { |node_key, pipeline| [node_key, build_thread_for_pipeline(@router.find_node(node_key), pipeline)] }
153
+ .each do |node_key, thread|
154
+ case v = thread.value
155
+ when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
156
+ all_replies ||= Array.new(@size)
157
+ pipeline = @pipelines[node_key]
158
+ v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
159
+ pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
160
+ when StandardError
161
+ errors ||= {}
162
+ errors[node_key] = v
163
+ else
164
+ all_replies ||= Array.new(@size)
165
+ @pipelines[node_key].outer_indices.each_with_index { |outer, inner| all_replies[outer] = v[inner] }
166
+ end
181
167
  end
182
- end
183
168
  end
184
169
 
185
170
  raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
@@ -197,6 +182,17 @@ class RedisClient
197
182
  @pipelines[node_key]
198
183
  end
199
184
 
185
+ def build_thread_for_pipeline(client, pipeline)
186
+ Thread.new(client, pipeline) do |cli, pl|
187
+ replies = do_pipelining(cli, pl)
188
+ raise ReplySizeError, "commands: #{pl._size}, replies: #{replies.size}" if pl._size != replies.size
189
+
190
+ replies
191
+ rescue StandardError => e
192
+ e
193
+ end
194
+ end
195
+
200
196
  def do_pipelining(client, pipeline)
201
197
  case client
202
198
  when ::RedisClient then send_pipeline(client, pipeline)
@@ -6,44 +6,44 @@ class RedisClient
6
6
  class Cluster
7
7
  class PubSub
8
8
  class State
9
- def initialize(client)
9
+ def initialize(client, queue)
10
10
  @client = client
11
11
  @worker = nil
12
+ @queue = queue
12
13
  end
13
14
 
14
15
  def call(command)
15
16
  @client.call_v(command)
16
17
  end
17
18
 
19
+ def ensure_worker
20
+ @worker = spawn_worker(@client, @queue) unless @worker&.alive?
21
+ end
22
+
18
23
  def close
19
24
  @worker.exit if @worker&.alive?
20
25
  @client.close
21
26
  end
22
27
 
23
- def take_message(timeout)
24
- @worker = subscribe(@client, timeout) if @worker.nil?
25
- return if @worker.alive?
26
-
27
- message = @worker[:reply]
28
- @worker = nil
29
- message
30
- end
31
-
32
28
  private
33
29
 
34
- def subscribe(client, timeout)
35
- Thread.new(client, timeout) do |pubsub, to|
36
- Thread.current[:reply] = pubsub.next_event(to)
37
- rescue StandardError => e
38
- Thread.current[:reply] = e
30
+ def spawn_worker(client, queue)
31
+ Thread.new(client, queue) do |pubsub, q|
32
+ loop do
33
+ q << pubsub.next_event
34
+ rescue StandardError => e
35
+ q << e
36
+ end
39
37
  end
40
38
  end
41
39
  end
42
40
 
41
+ BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
42
+
43
43
  def initialize(router, command_builder)
44
44
  @router = router
45
45
  @command_builder = command_builder
46
- @state_list = []
46
+ @queue = SizedQueue.new(BUF_SIZE)
47
47
  @state_dict = {}
48
48
  end
49
49
 
@@ -56,26 +56,25 @@ class RedisClient
56
56
  end
57
57
 
58
58
  def close
59
- @state_list.each(&:close)
60
- @state_list.clear
59
+ @state_dict.each_value(&:close)
61
60
  @state_dict.clear
61
+ @queue.clear
62
62
  end
63
63
 
64
64
  def next_event(timeout = nil)
65
- return if @state_list.empty?
66
-
67
- @state_list.shuffle!
65
+ @state_dict.each_value(&:ensure_worker)
68
66
  max_duration = calc_max_duration(timeout)
69
67
  starting = obtain_current_time
68
+
70
69
  loop do
71
70
  break if max_duration > 0 && obtain_current_time - starting > max_duration
72
71
 
73
- @state_list.each do |pubsub|
74
- message = pubsub.take_message(timeout)
75
- return message if message
72
+ case event = @queue.pop(true)
73
+ when StandardError then raise event
74
+ when Array then break event
76
75
  end
77
-
78
- sleep 0.001
76
+ rescue ThreadError
77
+ sleep 0.005
79
78
  end
80
79
  end
81
80
 
@@ -100,8 +99,7 @@ class RedisClient
100
99
  def add_state(node_key)
101
100
  return @state_dict[node_key] if @state_dict.key?(node_key)
102
101
 
103
- state = State.new(@router.find_node(node_key).pubsub)
104
- @state_list << state
102
+ state = State.new(@router.find_node(node_key).pubsub, @queue)
105
103
  @state_dict[node_key] = state
106
104
  end
107
105
 
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.4.16
4
+ version: 0.5.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: 2023-08-21 00:00:00.000000000 Z
11
+ date: 2023-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -40,6 +40,7 @@ files:
40
40
  - lib/redis_client/cluster/node/latency_replica.rb
41
41
  - lib/redis_client/cluster/node/primary_only.rb
42
42
  - lib/redis_client/cluster/node/random_replica.rb
43
+ - lib/redis_client/cluster/node/random_replica_or_primary.rb
43
44
  - lib/redis_client/cluster/node/replica_mixin.rb
44
45
  - lib/redis_client/cluster/node_key.rb
45
46
  - lib/redis_client/cluster/normalized_cmd_name.rb
@@ -69,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
70
  - !ruby/object:Gem::Version
70
71
  version: '0'
71
72
  requirements: []
72
- rubygems_version: 3.4.13
73
+ rubygems_version: 3.4.19
73
74
  signing_key:
74
75
  specification_version: 4
75
76
  summary: A Redis cluster client for Ruby