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 +4 -4
- data/lib/redis_client/cluster/node/latency_replica.rb +18 -21
- data/lib/redis_client/cluster/node/random_replica_or_primary.rb +55 -0
- data/lib/redis_client/cluster/node/replica_mixin.rb +2 -2
- data/lib/redis_client/cluster/node.rb +46 -41
- data/lib/redis_client/cluster/pipeline.rb +27 -31
- data/lib/redis_client/cluster/pub_sub.rb +26 -28
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ac7d6ca1474a42ad2d4027de72dbd087f6d248b3678ad2e360ed55b56dc60790
|
4
|
+
data.tar.gz: ce4de83f3f601772df8c95381a65172245967bd9568ff7959aec5858e55fb7af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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)
|
42
|
+
def measure_latencies(clients)
|
43
43
|
clients.each_slice(::RedisClient::Cluster::Node::MAX_THREADS).each_with_object({}) do |chuncked_clients, acc|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
63
|
-
|
64
|
-
|
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.
|
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
|
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
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
339
|
+
def try_map(clients, &block)
|
335
340
|
results = errors = nil
|
336
341
|
clients.each_slice(MAX_THREADS) do |chuncked_clients|
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
35
|
-
Thread.new(client,
|
36
|
-
|
37
|
-
|
38
|
-
|
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
|
-
@
|
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
|
-
@
|
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
|
-
|
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
|
-
@
|
74
|
-
|
75
|
-
|
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.
|
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
|
+
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-
|
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.
|
73
|
+
rubygems_version: 3.4.19
|
73
74
|
signing_key:
|
74
75
|
specification_version: 4
|
75
76
|
summary: A Redis cluster client for Ruby
|