redis-cluster-client 0.4.16 → 0.5.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: 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