redis-cluster-client 0.7.5 → 0.8.1

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: 8f07e3f65dcf4125b72089921a28fc2048d98bc95763d7ec2a5f0702e3ba542d
4
- data.tar.gz: a69bdc239562da7c18b6b8eedfbb16b51879416f0093eb472b35f212080ae29e
3
+ metadata.gz: 53d1591b89859ddc92dc6ca349ff211d0a6ac14c68ad72e920cc9725d82587b4
4
+ data.tar.gz: fcfeceffddc9201f8b8c5d6999d3e7aa8581c9f24f0d823b029a5561ef82887d
5
5
  SHA512:
6
- metadata.gz: 7afe4938ca64ebbcd4607144b0fb634a26c3ba99a463ded614da7f7b2a52e59a607c27949bcf4e746ce7ed31d71e3d37d204b054ffef6c8b35ed8f72a83f632e
7
- data.tar.gz: 876cc376f32d4c3d6d4b3c8c413e360fd91260bf173d80a90202428b7cef6923057da54df490111950cc278de49026a3f7726483d71bb24fd14e897e6c6f3f03
6
+ metadata.gz: d51aefc48e64ef0cdbded8b5c8e4b005dcfd103d9fa4ecfa0dff1d9aa4fed5d6f6aefd4dca4ac53e89867b5276812bc6c7f8cf3cb41c1184bd9d8f19ea601471
7
+ data.tar.gz: 548da631726b4881f0a7712186f656733cb6941aef9ca3415498a6f048f59170fb74a59c30da68d655cee5e5773732d9d781c489f6cda2f0c92a341f1b28b892
@@ -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
- key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
69
- hash_tag = extract_hash_tag(key)
70
- hash_tag.empty? ? key : hash_tag
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
- def determine_optional_key_position(command, option_name) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
- idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
106
- idx.nil? ? 0 : idx + 1
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
- # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
110
- def extract_hash_tag(key)
111
- key = key.to_s
112
- s = key.index(LEFT_BRACKET)
113
- return EMPTY_STRING if s.nil?
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
- key[s + 1..e - 1]
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,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ class Cluster
5
+ module ErrorIdentification
6
+ def self.client_owns_error?(err, client)
7
+ return true unless identifiable?(err)
8
+
9
+ err.from?(client)
10
+ end
11
+
12
+ def self.identifiable?(err)
13
+ err.is_a?(TaggedError)
14
+ end
15
+
16
+ module TaggedError
17
+ attr_accessor :config_instance
18
+
19
+ def from?(client)
20
+ client.config.equal?(config_instance)
21
+ end
22
+ end
23
+
24
+ module Middleware
25
+ def connect(config)
26
+ super
27
+ rescue RedisClient::Error => e
28
+ identify_error(e, config)
29
+ raise
30
+ end
31
+
32
+ def call(_command, config)
33
+ super
34
+ rescue RedisClient::Error => e
35
+ identify_error(e, config)
36
+ raise
37
+ end
38
+
39
+ def call_pipelined(_command, config)
40
+ super
41
+ rescue RedisClient::Error => e
42
+ identify_error(e, config)
43
+ raise
44
+ end
45
+
46
+ private
47
+
48
+ def identify_error(err, config)
49
+ err.singleton_class.include(TaggedError)
50
+ err.config_instance = config
51
+ end
52
+ end
53
+ end
54
+ end
55
+ 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/replica_mixin'
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
- private
33
-
34
- def build_clients(primary_node_keys, options, pool, **kwargs)
35
- options.filter_map do |node_key, option|
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/replica_mixin'
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/replica_mixin'
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) }