redis-cluster-client 0.7.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82e7044600bf789f17ab9db7287beb76ebdb8c1b24c3816523da64af6a836038
4
- data.tar.gz: ca55b62fc6a4252999f4fd0080cc267bfa31609eb8c9645265072bf374e61231
3
+ metadata.gz: 071a90a4437e12104fba1359e1a88fd64a0aa253f53ec1fdfd5c8b1fa62c63a5
4
+ data.tar.gz: 773243dc28547cc114730cbef7f0f971dd7bb178d4ea9a8cb2e8c4924f99bf40
5
5
  SHA512:
6
- metadata.gz: fe4e0e00d82ec9afab61a96265d1f0eb6f580ce6f9b1721df1135b2b298b21c99de46f0f04cba48a5173a299896897de80037e3256ed39f9605c6f51d3c3fd0c
7
- data.tar.gz: 956d3d22f40b00c6ec8608d06d33bb85e2ba0af810ffa68a30c51469423c7542eb52f33f71695dfe02a4c2e2577b4f870dd7684c920276adb205bf5df61630bd
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 = SLOW_COMMAND_TIMEOUT > 0.0 ? SLOW_COMMAND_TIMEOUT : regular_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
- key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
71
- hash_tag = extract_hash_tag(key)
72
- 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] }
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
- def determine_optional_key_position(command, option_name) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
107
- idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
108
- 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
109
144
  end
110
145
 
111
- # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
112
- def extract_hash_tag(key)
113
- key = key.to_s
114
- s = key.index(LEFT_BRACKET)
115
- return EMPTY_STRING if s.nil?
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
- 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
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/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) }