redis-cluster-client 0.7.5 → 0.7.6
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/command.rb +53 -17
- 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 -109
- 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 +49 -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 +30 -45
- 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,19 +2,21 @@
|
|
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
11
|
EMPTY_STRING = ''
|
11
|
-
LEFT_BRACKET = '{'
|
12
|
-
RIGHT_BRACKET = '}'
|
13
12
|
EMPTY_HASH = {}.freeze
|
13
|
+
EMPTY_ARRAY = [].freeze
|
14
14
|
|
15
15
|
Detail = Struct.new(
|
16
16
|
'RedisCommand',
|
17
17
|
:first_key_position,
|
18
|
+
:last_key_position,
|
19
|
+
:key_step,
|
18
20
|
:write?,
|
19
21
|
:readonly?,
|
20
22
|
keyword_init: true
|
@@ -50,6 +52,8 @@ class RedisClient
|
|
50
52
|
|
51
53
|
acc[row[0].downcase] = ::RedisClient::Cluster::Command::Detail.new(
|
52
54
|
first_key_position: row[3],
|
55
|
+
last_key_position: row[4],
|
56
|
+
key_step: row[5],
|
53
57
|
write?: row[2].include?('write'),
|
54
58
|
readonly?: row[2].include?('readonly')
|
55
59
|
)
|
@@ -65,9 +69,18 @@ class RedisClient
|
|
65
69
|
i = determine_first_key_position(command)
|
66
70
|
return EMPTY_STRING if i == 0
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
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] }
|
71
84
|
end
|
72
85
|
|
73
86
|
def should_send_to_primary?(command)
|
@@ -101,21 +114,44 @@ class RedisClient
|
|
101
114
|
end
|
102
115
|
end
|
103
116
|
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
107
144
|
end
|
108
145
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
e = key.index(RIGHT_BRACKET, s + 1)
|
116
|
-
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
|
117
151
|
|
118
|
-
|
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
|
119
155
|
end
|
120
156
|
end
|
121
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) }
|