redis-cluster-client 0.7.4 → 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/redis_client/cluster/command.rb +55 -21
- data/lib/redis_client/cluster/error_identification.rb +49 -0
- data/lib/redis_client/cluster/key_slot_converter.rb +18 -0
- data/lib/redis_client/cluster/node/base_topology.rb +60 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +13 -17
- data/lib/redis_client/cluster/node/primary_only.rb +7 -19
- data/lib/redis_client/cluster/node/random_replica.rb +2 -4
- data/lib/redis_client/cluster/node/random_replica_or_primary.rb +2 -4
- data/lib/redis_client/cluster/node.rb +169 -112
- data/lib/redis_client/cluster/pinning_node.rb +35 -0
- data/lib/redis_client/cluster/pipeline.rb +19 -18
- data/lib/redis_client/cluster/router.rb +52 -83
- data/lib/redis_client/cluster/transaction.rb +158 -20
- data/lib/redis_client/cluster.rb +28 -2
- data/lib/redis_client/cluster_config.rb +35 -42
- metadata +6 -4
- data/lib/redis_client/cluster/node/replica_mixin.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 071a90a4437e12104fba1359e1a88fd64a0aa253f53ec1fdfd5c8b1fa62c63a5
|
4
|
+
data.tar.gz: 773243dc28547cc114730cbef7f0f971dd7bb178d4ea9a8cb2e8c4924f99bf40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d345690443bc35cf19935ae9e886e0833bee49b772f21abdfb4752629b7c436100e6a367b730055fbd155d59924cfd4d76ad3df2ee1a0ed3aff65f097a82a208
|
7
|
+
data.tar.gz: d76e747b3eec56184f253f40f82599f492338fe86d4d3c3d30e9cda22df087e6817673b7bf9f02c07192b77322efd7d204ff60ced77be7fe2e8764cf9d104f63
|
@@ -2,33 +2,33 @@
|
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
4
|
require 'redis_client/cluster/errors'
|
5
|
+
require 'redis_client/cluster/key_slot_converter'
|
5
6
|
require 'redis_client/cluster/normalized_cmd_name'
|
6
7
|
|
7
8
|
class RedisClient
|
8
9
|
class Cluster
|
9
10
|
class Command
|
10
|
-
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
11
|
-
|
12
11
|
EMPTY_STRING = ''
|
13
|
-
LEFT_BRACKET = '{'
|
14
|
-
RIGHT_BRACKET = '}'
|
15
12
|
EMPTY_HASH = {}.freeze
|
13
|
+
EMPTY_ARRAY = [].freeze
|
16
14
|
|
17
15
|
Detail = Struct.new(
|
18
16
|
'RedisCommand',
|
19
17
|
:first_key_position,
|
18
|
+
:last_key_position,
|
19
|
+
:key_step,
|
20
20
|
:write?,
|
21
21
|
:readonly?,
|
22
22
|
keyword_init: true
|
23
23
|
)
|
24
24
|
|
25
25
|
class << self
|
26
|
-
def load(nodes)
|
26
|
+
def load(nodes, slow_command_timeout: -1)
|
27
27
|
cmd = errors = nil
|
28
28
|
|
29
29
|
nodes&.each do |node|
|
30
30
|
regular_timeout = node.read_timeout
|
31
|
-
node.read_timeout =
|
31
|
+
node.read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
|
32
32
|
reply = node.call('COMMAND')
|
33
33
|
node.read_timeout = regular_timeout
|
34
34
|
commands = parse_command_reply(reply)
|
@@ -52,6 +52,8 @@ class RedisClient
|
|
52
52
|
|
53
53
|
acc[row[0].downcase] = ::RedisClient::Cluster::Command::Detail.new(
|
54
54
|
first_key_position: row[3],
|
55
|
+
last_key_position: row[4],
|
56
|
+
key_step: row[5],
|
55
57
|
write?: row[2].include?('write'),
|
56
58
|
readonly?: row[2].include?('readonly')
|
57
59
|
)
|
@@ -67,9 +69,18 @@ class RedisClient
|
|
67
69
|
i = determine_first_key_position(command)
|
68
70
|
return EMPTY_STRING if i == 0
|
69
71
|
|
70
|
-
|
71
|
-
|
72
|
-
|
72
|
+
(command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
|
73
|
+
end
|
74
|
+
|
75
|
+
def extract_all_keys(command)
|
76
|
+
keys_start = determine_first_key_position(command)
|
77
|
+
keys_end = determine_last_key_position(command, keys_start)
|
78
|
+
keys_step = determine_key_step(command)
|
79
|
+
return EMPTY_ARRAY if [keys_start, keys_end, keys_step].any?(&:zero?)
|
80
|
+
|
81
|
+
keys_end = [keys_end, command.size - 1].min
|
82
|
+
# use .. inclusive range because keys_end is a valid index.
|
83
|
+
(keys_start..keys_end).step(keys_step).map { |i| command[i] }
|
73
84
|
end
|
74
85
|
|
75
86
|
def should_send_to_primary?(command)
|
@@ -103,21 +114,44 @@ class RedisClient
|
|
103
114
|
end
|
104
115
|
end
|
105
116
|
|
106
|
-
|
107
|
-
|
108
|
-
|
117
|
+
# IMPORTANT: this determines the last key position INCLUSIVE of the last key -
|
118
|
+
# i.e. command[determine_last_key_position(command)] is a key.
|
119
|
+
# This is in line with what Redis returns from COMMANDS.
|
120
|
+
def determine_last_key_position(command, keys_start) # rubocop:disable Metrics/AbcSize
|
121
|
+
case name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
122
|
+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore'
|
123
|
+
# EVALSHA sha1 numkeys [key [key ...]] [arg [arg ...]]
|
124
|
+
# ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
|
125
|
+
command[2].to_i + 2
|
126
|
+
when 'object', 'memory'
|
127
|
+
# OBJECT [ENCODING | FREQ | IDLETIME | REFCOUNT] key
|
128
|
+
# MEMORY USAGE key [SAMPLES count]
|
129
|
+
keys_start
|
130
|
+
when 'migrate'
|
131
|
+
# MIGRATE host port <key | ""> destination-db timeout [COPY] [REPLACE] [AUTH password | AUTH2 username password] [KEYS key [key ...]]
|
132
|
+
command[3].empty? ? (command.length - 1) : 3
|
133
|
+
when 'xread', 'xreadgroup'
|
134
|
+
# XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
|
135
|
+
keys_start + ((command.length - keys_start) / 2) - 1
|
136
|
+
else
|
137
|
+
# If there is a fixed, non-variable number of keys, don't iterate past that.
|
138
|
+
if @commands[name].last_key_position >= 0
|
139
|
+
@commands[name].last_key_position
|
140
|
+
else
|
141
|
+
command.length + @commands[name].last_key_position
|
142
|
+
end
|
143
|
+
end
|
109
144
|
end
|
110
145
|
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
e = key.index(RIGHT_BRACKET, s + 1)
|
118
|
-
return EMPTY_STRING if e.nil?
|
146
|
+
def determine_key_step(command)
|
147
|
+
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
|
148
|
+
# Some commands like EVALSHA have zero as the step in COMMANDS somehow.
|
149
|
+
@commands[name].key_step == 0 ? 1 : @commands[name].key_step
|
150
|
+
end
|
119
151
|
|
120
|
-
|
152
|
+
def determine_optional_key_position(command, option_name) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
153
|
+
idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
|
154
|
+
idx.nil? ? 0 : idx + 1
|
121
155
|
end
|
122
156
|
end
|
123
157
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
class Cluster
|
5
|
+
module ErrorIdentification
|
6
|
+
def self.client_owns_error?(err, client)
|
7
|
+
err.is_a?(TaggedError) && err.from?(client)
|
8
|
+
end
|
9
|
+
|
10
|
+
module TaggedError
|
11
|
+
attr_accessor :config_instance
|
12
|
+
|
13
|
+
def from?(client)
|
14
|
+
client.config.equal?(config_instance)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Middleware
|
19
|
+
def connect(config)
|
20
|
+
super
|
21
|
+
rescue RedisClient::Error => e
|
22
|
+
identify_error(e, config)
|
23
|
+
raise
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(_command, config)
|
27
|
+
super
|
28
|
+
rescue RedisClient::Error => e
|
29
|
+
identify_error(e, config)
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
|
33
|
+
def call_pipelined(_command, config)
|
34
|
+
super
|
35
|
+
rescue RedisClient::Error => e
|
36
|
+
identify_error(e, config)
|
37
|
+
raise
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def identify_error(err, config)
|
43
|
+
err.singleton_class.include(TaggedError)
|
44
|
+
err.config_instance = config
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -3,6 +3,9 @@
|
|
3
3
|
class RedisClient
|
4
4
|
class Cluster
|
5
5
|
module KeySlotConverter
|
6
|
+
EMPTY_STRING = ''
|
7
|
+
LEFT_BRACKET = '{'
|
8
|
+
RIGHT_BRACKET = '}'
|
6
9
|
XMODEM_CRC16_LOOKUP = [
|
7
10
|
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
|
8
11
|
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
|
@@ -45,6 +48,9 @@ class RedisClient
|
|
45
48
|
def convert(key)
|
46
49
|
return nil if key.nil?
|
47
50
|
|
51
|
+
hash_tag = extract_hash_tag(key)
|
52
|
+
key = hash_tag unless hash_tag.empty?
|
53
|
+
|
48
54
|
crc = 0
|
49
55
|
key.each_byte do |b|
|
50
56
|
crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
|
@@ -52,6 +58,18 @@ class RedisClient
|
|
52
58
|
|
53
59
|
crc % HASH_SLOTS
|
54
60
|
end
|
61
|
+
|
62
|
+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
|
63
|
+
def extract_hash_tag(key)
|
64
|
+
key = key.to_s
|
65
|
+
s = key.index(LEFT_BRACKET)
|
66
|
+
return EMPTY_STRING if s.nil?
|
67
|
+
|
68
|
+
e = key.index(RIGHT_BRACKET, s + 1)
|
69
|
+
return EMPTY_STRING if e.nil?
|
70
|
+
|
71
|
+
key[s + 1..e - 1]
|
72
|
+
end
|
55
73
|
end
|
56
74
|
end
|
57
75
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class RedisClient
|
4
|
+
class Cluster
|
5
|
+
class Node
|
6
|
+
class BaseTopology
|
7
|
+
IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
|
8
|
+
attr_reader :clients, :primary_clients, :replica_clients
|
9
|
+
|
10
|
+
def initialize(pool, concurrent_worker, **kwargs)
|
11
|
+
@pool = pool
|
12
|
+
@clients = {}
|
13
|
+
@client_options = kwargs.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
|
14
|
+
@concurrent_worker = concurrent_worker
|
15
|
+
@replications = EMPTY_HASH
|
16
|
+
@primary_node_keys = EMPTY_ARRAY
|
17
|
+
@replica_node_keys = EMPTY_ARRAY
|
18
|
+
@primary_clients = EMPTY_ARRAY
|
19
|
+
@replica_clients = EMPTY_ARRAY
|
20
|
+
end
|
21
|
+
|
22
|
+
def any_primary_node_key(seed: nil)
|
23
|
+
random = seed.nil? ? Random : Random.new(seed)
|
24
|
+
@primary_node_keys.sample(random: random)
|
25
|
+
end
|
26
|
+
|
27
|
+
def process_topology_update!(replications, options) # rubocop:disable Metrics/AbcSize
|
28
|
+
@replications = replications.freeze
|
29
|
+
@primary_node_keys = @replications.keys.sort.select { |k| options.key?(k) }.freeze
|
30
|
+
@replica_node_keys = @replications.values.flatten.sort.select { |k| options.key?(k) }.freeze
|
31
|
+
|
32
|
+
# Disconnect from nodes that we no longer want, and connect to nodes we're not connected to yet
|
33
|
+
disconnect_from_unwanted_nodes(options)
|
34
|
+
connect_to_new_nodes(options)
|
35
|
+
|
36
|
+
@primary_clients, @replica_clients = @clients.partition { |k, _| @primary_node_keys.include?(k) }.map(&:to_h)
|
37
|
+
@primary_clients.freeze
|
38
|
+
@replica_clients.freeze
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def disconnect_from_unwanted_nodes(options)
|
44
|
+
(@clients.keys - options.keys).each do |node_key|
|
45
|
+
@clients.delete(node_key).close
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def connect_to_new_nodes(options)
|
50
|
+
(options.keys - @clients.keys).each do |node_key|
|
51
|
+
option = options[node_key].merge(@client_options)
|
52
|
+
config = ::RedisClient::Cluster::Node::Config.new(scale_read: !@primary_node_keys.include?(node_key), **option)
|
53
|
+
client = @pool.nil? ? config.new_client : config.new_pool(**@pool)
|
54
|
+
@clients[node_key] = client
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -1,29 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'redis_client/cluster/node/
|
3
|
+
require 'redis_client/cluster/node/base_topology'
|
4
4
|
|
5
5
|
class RedisClient
|
6
6
|
class Cluster
|
7
7
|
class Node
|
8
|
-
class LatencyReplica
|
9
|
-
include ::RedisClient::Cluster::Node::ReplicaMixin
|
10
|
-
|
11
|
-
attr_reader :replica_clients
|
12
|
-
|
8
|
+
class LatencyReplica < BaseTopology
|
13
9
|
DUMMY_LATENCY_MSEC = 100 * 1000 * 1000
|
14
10
|
MEASURE_ATTEMPT_COUNT = 10
|
15
11
|
|
16
|
-
def initialize(replications, options, pool, concurrent_worker, **kwargs)
|
17
|
-
super
|
18
|
-
|
19
|
-
all_replica_clients = @clients.select { |k, _| @replica_node_keys.include?(k) }
|
20
|
-
latencies = measure_latencies(all_replica_clients, concurrent_worker)
|
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.values.reject(&:empty?)
|
25
|
-
end
|
26
|
-
|
27
12
|
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
28
13
|
@clients_for_scanning
|
29
14
|
end
|
@@ -37,6 +22,17 @@ class RedisClient
|
|
37
22
|
@existed_replicas.sample(random: random)&.first || any_primary_node_key(seed: seed)
|
38
23
|
end
|
39
24
|
|
25
|
+
def process_topology_update!(replications, options)
|
26
|
+
super
|
27
|
+
|
28
|
+
all_replica_clients = @clients.select { |k, _| @replica_node_keys.include?(k) }
|
29
|
+
latencies = measure_latencies(all_replica_clients, @concurrent_worker)
|
30
|
+
@replications.each_value { |keys| keys.sort_by! { |k| latencies.fetch(k) } }
|
31
|
+
@replica_clients = select_replica_clients(@replications, @clients)
|
32
|
+
@clients_for_scanning = select_clients_for_scanning(@replications, @clients)
|
33
|
+
@existed_replicas = @replications.values.reject(&:empty?)
|
34
|
+
end
|
35
|
+
|
40
36
|
private
|
41
37
|
|
42
38
|
def measure_latencies(clients, concurrent_worker) # rubocop:disable Metrics/AbcSize
|
@@ -1,16 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'redis_client/cluster/node/base_topology'
|
4
|
+
|
3
5
|
class RedisClient
|
4
6
|
class Cluster
|
5
7
|
class Node
|
6
|
-
class PrimaryOnly
|
7
|
-
attr_reader :clients
|
8
|
-
|
9
|
-
def initialize(replications, options, pool, _concurrent_worker, **kwargs)
|
10
|
-
@primary_node_keys = replications.keys.sort
|
11
|
-
@clients = build_clients(@primary_node_keys, options, pool, **kwargs)
|
12
|
-
end
|
13
|
-
|
8
|
+
class PrimaryOnly < BaseTopology
|
14
9
|
alias primary_clients clients
|
15
10
|
alias replica_clients clients
|
16
11
|
|
@@ -29,17 +24,10 @@ class RedisClient
|
|
29
24
|
|
30
25
|
alias any_replica_node_key any_primary_node_key
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
27
|
+
def process_topology_update!(replications, options)
|
28
|
+
# Remove non-primary nodes from options (provided that we actually have any primaries at all)
|
29
|
+
options = options.select { |node_key, _| replications.key?(node_key) } if replications.keys.any?
|
30
|
+
super(replications, options)
|
43
31
|
end
|
44
32
|
end
|
45
33
|
end
|
@@ -1,13 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'redis_client/cluster/node/
|
3
|
+
require 'redis_client/cluster/node/base_topology'
|
4
4
|
|
5
5
|
class RedisClient
|
6
6
|
class Cluster
|
7
7
|
class Node
|
8
|
-
class RandomReplica
|
9
|
-
include ::RedisClient::Cluster::Node::ReplicaMixin
|
10
|
-
|
8
|
+
class RandomReplica < BaseTopology
|
11
9
|
def replica_clients
|
12
10
|
keys = @replications.values.filter_map(&:sample)
|
13
11
|
@clients.select { |k, _| keys.include?(k) }
|
@@ -1,13 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'redis_client/cluster/node/
|
3
|
+
require 'redis_client/cluster/node/base_topology'
|
4
4
|
|
5
5
|
class RedisClient
|
6
6
|
class Cluster
|
7
7
|
class Node
|
8
|
-
class RandomReplicaOrPrimary
|
9
|
-
include ::RedisClient::Cluster::Node::ReplicaMixin
|
10
|
-
|
8
|
+
class RandomReplicaOrPrimary < BaseTopology
|
11
9
|
def replica_clients
|
12
10
|
keys = @replications.values.filter_map(&:sample)
|
13
11
|
@clients.select { |k, _| keys.include?(k) }
|