redis-cluster-client 0.3.7 → 0.3.9

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: 5243f77ab4cb1322ddaaade9b001780e8f24d702d293f56870ce9e1109780222
4
- data.tar.gz: fabae20f3a0d9bede270602adb247ca3cbe2b36ad42d68ecdd908320b18a31bb
3
+ metadata.gz: 9570ec36f046f18840242eb63ede8e5bdf710e4a9a2d738b244bed46ea53530c
4
+ data.tar.gz: 5fd06dcf1f843e7f6151278400f33d45725c1ee1b8dcbf0afc6d72a00677c237
5
5
  SHA512:
6
- metadata.gz: 999a6b334092b0e498d5e8ba773131542d212d8e5b8cb7b117811c465ccb4385b5bea11637cd8e4febdebc3d1e33c218b084e13bd5e5f1686ab477dca0f9411f
7
- data.tar.gz: b7edede3965d1878459a128a3e7cc0f5c84b9b3e434ec44c86b408830670058b28ec023ffeeb2f0bcd1503ba5c274c46ecf791f5b03ebbdcccff09c3e3b1cf06
6
+ metadata.gz: 0c4b639bb180399c9d71bd054da5d10c03e93fafd4f595aa958dbec36d842a8b01da9449f5fe57f4c51a6088a99abc35fc2e6e5ec1605ba7058ecab8b05a9ebd
7
+ data.tar.gz: 50b5b38b74b9b79b9526af4f76fdf7b3a5b54a51ccf2a641c78039610478087cbb0452239454580288a66c580cdacb0277f6aaf14f2eb44c5804ae6f86607b3d
@@ -9,6 +9,14 @@ class RedisClient
9
9
  class Command
10
10
  EMPTY_STRING = ''
11
11
 
12
+ Detail = Struct.new(
13
+ 'RedisCommand',
14
+ :first_key_position,
15
+ :write?,
16
+ :readonly?,
17
+ keyword_init: true
18
+ )
19
+
12
20
  class << self
13
21
  def load(nodes)
14
22
  errors = []
@@ -17,8 +25,8 @@ class RedisClient
17
25
  break unless cmd.nil?
18
26
 
19
27
  reply = node.call('COMMAND')
20
- details = parse_command_details(reply)
21
- cmd = ::RedisClient::Cluster::Command.new(details)
28
+ commands = parse_command_reply(reply)
29
+ cmd = ::RedisClient::Cluster::Command.new(commands)
22
30
  rescue ::RedisClient::Error => e
23
31
  errors << e
24
32
  end
@@ -30,15 +38,22 @@ class RedisClient
30
38
 
31
39
  private
32
40
 
33
- def parse_command_details(rows)
41
+ def parse_command_reply(rows)
34
42
  rows&.reject { |row| row[0].nil? }.to_h do |row|
35
- [row[0].downcase, { arity: row[1], flags: row[2], first: row[3], last: row[4], step: row[5] }]
43
+ [
44
+ row[0].downcase,
45
+ ::RedisClient::Cluster::Command::Detail.new(
46
+ first_key_position: row[3],
47
+ write?: row[2].include?('write'),
48
+ readonly?: row[2].include?('readonly')
49
+ )
50
+ ]
36
51
  end
37
52
  end
38
53
  end
39
54
 
40
- def initialize(details)
41
- @details = pick_details(details)
55
+ def initialize(commands)
56
+ @commands = commands || {}
42
57
  end
43
58
 
44
59
  def extract_first_key(command)
@@ -51,39 +66,23 @@ class RedisClient
51
66
  end
52
67
 
53
68
  def should_send_to_primary?(command)
54
- dig_details(command, :write)
69
+ name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
70
+ @commands[name]&.write?
55
71
  end
56
72
 
57
73
  def should_send_to_replica?(command)
58
- dig_details(command, :readonly)
74
+ name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
75
+ @commands[name]&.readonly?
59
76
  end
60
77
 
61
78
  def exists?(name)
62
- key = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name)
63
- @details.key?(key)
79
+ @commands.key?(::RedisClient::Cluster::NormalizedCmdName.instance.get_by_name(name))
64
80
  end
65
81
 
66
82
  private
67
83
 
68
- def pick_details(details)
69
- (details || {}).transform_values do |detail|
70
- {
71
- first_key_position: detail[:first],
72
- write: detail[:flags].include?('write'),
73
- readonly: detail[:flags].include?('readonly')
74
- }
75
- end
76
- end
77
-
78
- def dig_details(command, key)
79
- name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
80
- return if name.empty? || !@details.key?(name)
81
-
82
- @details.fetch(name).fetch(key)
83
- end
84
-
85
84
  def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticComplexity
86
- case ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
85
+ case name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
87
86
  when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
88
87
  when 'object' then 2
89
88
  when 'memory'
@@ -93,7 +92,7 @@ class RedisClient
93
92
  when 'xread', 'xreadgroup'
94
93
  determine_optional_key_position(command, 'streams')
95
94
  else
96
- dig_details(command, :first_key_position).to_i
95
+ @commands[name]&.first_key_position.to_i
97
96
  end
98
97
  end
99
98
 
@@ -34,7 +34,7 @@ class RedisClient
34
34
 
35
35
  def any_replica_node_key(seed: nil)
36
36
  random = seed.nil? ? Random : Random.new(seed)
37
- @existed_replicas.sample(random: random)&.first
37
+ @existed_replicas.sample(random: random)&.first || any_primary_node_key(seed: seed)
38
38
  end
39
39
 
40
40
  private
@@ -29,7 +29,7 @@ class RedisClient
29
29
 
30
30
  def any_replica_node_key(seed: nil)
31
31
  random = seed.nil? ? Random : Random.new(seed)
32
- @replica_node_keys.sample(random: random)
32
+ @replica_node_keys.sample(random: random) || any_primary_node_key(seed: seed)
33
33
  end
34
34
  end
35
35
  end
@@ -20,6 +20,40 @@ class RedisClient
20
20
  IGNORE_GENERIC_CONFIG_KEYS = %i[url host port path].freeze
21
21
 
22
22
  ReloadNeeded = Class.new(::RedisClient::Error)
23
+ Info = Struct.new(
24
+ 'RedisNode',
25
+ :id, :node_key, :role, :primary_id, :ping_sent,
26
+ :pong_recv, :config_epoch, :link_state, :slots,
27
+ keyword_init: true
28
+ ) do
29
+ def primary?
30
+ role == 'master'
31
+ end
32
+
33
+ def replica?
34
+ role == 'slave'
35
+ end
36
+ end
37
+
38
+ SLOT_OPTIMIZATION_MAX_SHARD_SIZE = 256
39
+ SLOT_OPTIMIZATION_STRING = '0' * SLOT_SIZE
40
+ Slot = Struct.new('RedisSlot', :slots, :primary_node_keys, keyword_init: true) do
41
+ def [](slot)
42
+ primary_node_keys[slots.getbyte(slot)]
43
+ end
44
+
45
+ def []=(slot, primary_node_key)
46
+ index = primary_node_keys.find_index(primary_node_key)
47
+ if index.nil?
48
+ raise(::RedisClient::Cluster::Node::ReloadNeeded, primary_node_key) if primary_node_keys.size >= SLOT_OPTIMIZATION_MAX_SHARD_SIZE
49
+
50
+ index = primary_node_keys.size
51
+ primary_node_keys << primary_node_key
52
+ end
53
+
54
+ slots.setbyte(slot, index)
55
+ end
56
+ end
23
57
 
24
58
  class Config < ::RedisClient::Config
25
59
  def initialize(scale_read: false, **kwargs)
@@ -48,7 +82,7 @@ class RedisClient
48
82
  Thread.pass
49
83
  Thread.current.thread_variable_set(:index, i)
50
84
  reply = cli.call('CLUSTER', 'NODES')
51
- Thread.current.thread_variable_set(:info, parse_node_info(reply))
85
+ Thread.current.thread_variable_set(:info, parse_cluster_node_reply(reply))
52
86
  rescue StandardError => e
53
87
  Thread.current.thread_variable_set(:error, e)
54
88
  ensure
@@ -70,10 +104,11 @@ class RedisClient
70
104
 
71
105
  raise ::RedisClient::Cluster::InitialSetupError, errors if node_info_list.nil?
72
106
 
73
- grouped = node_info_list.compact.group_by do |rows|
74
- rows.sort_by { |row| row[:id] }
75
- .map { |r| "#{r[:id]}#{r[:node_key]}#{r[:role]}#{r[:primary_id]}#{r[:config_epoch]}" }
76
- .join
107
+ grouped = node_info_list.compact.group_by do |info_list|
108
+ info_list
109
+ .sort_by(&:id)
110
+ .map { |i| "#{i.id}#{i.node_key}#{i.role}#{i.primary_id}#{i.config_epoch}" }
111
+ .join
77
112
  end
78
113
 
79
114
  grouped.max_by { |_, v| v.size }[1].first
@@ -83,8 +118,8 @@ class RedisClient
83
118
 
84
119
  # @see https://redis.io/commands/cluster-nodes/
85
120
  # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
86
- def parse_node_info(info) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
87
- rows = info.split("\n").map(&:split)
121
+ def parse_cluster_node_reply(reply) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
122
+ rows = reply.split("\n").map(&:split)
88
123
  rows.each { |arr| arr[2] = arr[2].split(',') }
89
124
  rows.select! { |arr| arr[7] == 'connected' && (arr[2] & %w[fail? fail handshake noaddr noflags]).empty? }
90
125
  rows.each do |arr|
@@ -99,23 +134,25 @@ class RedisClient
99
134
  end
100
135
 
101
136
  rows.map do |arr|
102
- { id: arr[0], node_key: arr[1], role: arr[2], primary_id: arr[3], ping_sent: arr[4],
103
- pong_recv: arr[5], config_epoch: arr[6], link_state: arr[7], slots: arr[8] }
137
+ ::RedisClient::Cluster::Node::Info.new(
138
+ id: arr[0], node_key: arr[1], role: arr[2], primary_id: arr[3], ping_sent: arr[4],
139
+ pong_recv: arr[5], config_epoch: arr[6], link_state: arr[7], slots: arr[8]
140
+ )
104
141
  end
105
142
  end
106
143
  end
107
144
 
108
- def initialize( # rubocop:disable Metrics/ParameterLists
145
+ def initialize(
109
146
  options,
110
- node_info: [],
147
+ node_info_list: [],
111
148
  with_replica: false,
112
149
  replica_affinity: :random,
113
150
  pool: nil,
114
151
  **kwargs
115
152
  )
116
153
 
117
- @slots = build_slot_node_mappings(node_info)
118
- @replications = build_replication_mappings(node_info)
154
+ @slots = build_slot_node_mappings(node_info_list)
155
+ @replications = build_replication_mappings(node_info_list)
119
156
  @topology = make_topology_class(with_replica, replica_affinity).new(@replications, options, pool, **kwargs)
120
157
  @mutex = Mutex.new
121
158
  end
@@ -207,23 +244,32 @@ class RedisClient
207
244
  end
208
245
  end
209
246
 
210
- def build_slot_node_mappings(node_info)
211
- slots = Array.new(SLOT_SIZE)
212
- node_info.each do |info|
213
- next if info[:slots].nil? || info[:slots].empty?
247
+ def build_slot_node_mappings(node_info_list)
248
+ slots = make_array_for_slot_node_mappings(node_info_list)
249
+ node_info_list.each do |info|
250
+ next if info.slots.nil? || info.slots.empty?
214
251
 
215
- info[:slots].each { |start, last| (start..last).each { |i| slots[i] = info[:node_key] } }
252
+ info.slots.each { |start, last| (start..last).each { |i| slots[i] = info.node_key } }
216
253
  end
217
254
 
218
255
  slots
219
256
  end
220
257
 
221
- def build_replication_mappings(node_info) # rubocop:disable Metrics/AbcSize
222
- dict = node_info.to_h { |info| [info[:id], info] }
223
- node_info.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
224
- primary_info = dict[info[:primary_id]]
225
- acc[primary_info[:node_key]] << info[:node_key] unless primary_info.nil?
226
- acc[info[:node_key]] if info[:role] == 'master' # for the primary which have no replicas
258
+ def make_array_for_slot_node_mappings(node_info_list)
259
+ return Array.new(SLOT_SIZE) if node_info_list.count(&:primary?) > SLOT_OPTIMIZATION_MAX_SHARD_SIZE
260
+
261
+ ::RedisClient::Cluster::Node::Slot.new(
262
+ slots: String.new(SLOT_OPTIMIZATION_STRING, encoding: Encoding::BINARY, capacity: SLOT_SIZE),
263
+ primary_node_keys: node_info_list.select(&:primary?).map(&:node_key)
264
+ )
265
+ end
266
+
267
+ def build_replication_mappings(node_info_list) # rubocop:disable Metrics/AbcSize
268
+ dict = node_info_list.to_h { |info| [info.id, info] }
269
+ node_info_list.each_with_object(Hash.new { |h, k| h[k] = [] }) do |info, acc|
270
+ primary_info = dict[info.primary_id]
271
+ acc[primary_info.node_key] << info.node_key unless primary_info.nil?
272
+ acc[info.node_key] if info.primary? # for the primary which have no replicas
227
273
  end
228
274
  end
229
275
 
@@ -57,11 +57,12 @@ class RedisClient
57
57
  end
58
58
 
59
59
  def normalize(name)
60
- return @cache[name] if @cache.key?(name)
60
+ return @cache[name] || name.to_s.downcase if @cache.key?(name)
61
61
  return name.to_s.downcase if @mutex.locked?
62
62
 
63
- @mutex.synchronize { @cache[name] = name.to_s.downcase }
64
- @cache[name]
63
+ str = name.to_s.downcase
64
+ @mutex.synchronize { @cache[name] = str }
65
+ str
65
66
  end
66
67
  end
67
68
  end
@@ -63,8 +63,7 @@ class RedisClient
63
63
  raise
64
64
  end
65
65
 
66
- # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
67
- # Redirection and resharding
66
+ # @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
68
67
  def try_send(node, method, command, args, retry_count: 3, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
69
68
  if args.empty?
70
69
  # prevent memory allocation for variable-length args
@@ -271,12 +270,12 @@ class RedisClient
271
270
  end
272
271
 
273
272
  def fetch_cluster_info(config, pool: nil, **kwargs)
274
- node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
275
- node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
273
+ node_info_list = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
274
+ node_addrs = node_info_list.map { |i| ::RedisClient::Cluster::NodeKey.hashify(i.node_key) }
276
275
  config.update_node(node_addrs)
277
276
  ::RedisClient::Cluster::Node.new(
278
277
  config.per_node_key,
279
- node_info: node_info,
278
+ node_info_list: node_info_list,
280
279
  pool: pool,
281
280
  with_replica: config.use_replica?,
282
281
  replica_affinity: config.replica_affinity,
@@ -22,7 +22,7 @@ class RedisClient
22
22
 
23
23
  attr_reader :command_builder, :client_config, :replica_affinity
24
24
 
25
- def initialize( # rubocop:disable Metrics/ParameterLists
25
+ def initialize(
26
26
  nodes: DEFAULT_NODES,
27
27
  replica: false,
28
28
  replica_affinity: :random,
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.3.7
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Taishi Kasuga
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-09-22 00:00:00.000000000 Z
11
+ date: 2022-09-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client