redis-cluster-client 0.0.9 → 0.0.12

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: 032431ff16773725b9cb68482bc5df78affdf23e66b6bbd91b6ddbabdd9da921
4
- data.tar.gz: dc789f59f6509e35ab23df03a70867b5e8414ee3bb327462fbcd5d63be4c6744
3
+ metadata.gz: 2aec24d7a41cc222cf5fdec13b063d0ce2a68df59c05049a4719f71b6ac07391
4
+ data.tar.gz: 00b0eebd7da92305fbf2512f660721ad0976cfb1e33fb72037a673fe65899ce1
5
5
  SHA512:
6
- metadata.gz: 6013ff65d8dc06b776b7b201244d75eaf62993368f5285d823472f78bec2c81639734f23c32793bc1b8d1981905e5794e35e4e353696d70f1d66930434e6104f
7
- data.tar.gz: 0e1dc5f17a9706c2fc64bdd03e74ee457f9b9aea81740cdeb5459bd9bbc769b6353047b64ef008b6d34b950fbc7cb8cf57e66c9deb41d33edbacaba78ca2a12e
6
+ metadata.gz: 28d31d104bd1f2100c0f61c765783e86fea745910c748510bdc7b4985f779df4e8d8240a38afc043504cc36ad33a00273288762defcdcd03e7486cece5351882
7
+ data.tar.gz: 82a6c2b2e786f27183cc000ca202e4d6bf29b6260dc00a2659eb18660e6e4b3d60946d170606938242dc9340b4ae644aaa15de7369c23e2f9cda0c2bab604fe8
@@ -7,15 +7,21 @@ class RedisClient
7
7
  class Cluster
8
8
  class Command
9
9
  class << self
10
- def load(nodes)
11
- errors = nodes&.map do |node|
10
+ def load(nodes) # rubocop:disable Metrics/MethodLength
11
+ errors = []
12
+ cmd = nil
13
+ nodes&.each do |node|
14
+ break unless cmd.nil?
15
+
12
16
  reply = node.call('COMMAND')
13
17
  details = parse_command_details(reply)
14
- return ::RedisClient::Cluster::Command.new(details)
15
- rescue ::RedisClient::ConnectionError, ::RedisClient::CommandError => e
16
- e
18
+ cmd = ::RedisClient::Cluster::Command.new(details)
19
+ rescue ::RedisClient::Error => e
20
+ errors << e
17
21
  end
18
22
 
23
+ return cmd unless cmd.nil?
24
+
19
25
  raise ::RedisClient::Cluster::InitialSetupError, errors
20
26
  end
21
27
 
@@ -75,7 +81,7 @@ class RedisClient
75
81
  when 'memory'
76
82
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
77
83
  when 'migrate'
78
- command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
84
+ command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
79
85
  when 'xread', 'xreadgroup'
80
86
  determine_optional_key_position(command, 'streams')
81
87
  else
@@ -6,8 +6,6 @@ class RedisClient
6
6
  class Cluster
7
7
  ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
8
8
 
9
- # Raised when client connected to redis as cluster mode
10
- # and failed to fetch cluster state information by commands.
11
9
  class InitialSetupError < ::RedisClient::Error
12
10
  def initialize(errors)
13
11
  msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
@@ -15,20 +13,17 @@ class RedisClient
15
13
  end
16
14
  end
17
15
 
18
- # Raised when client connected to redis as cluster mode
19
- # and some cluster subcommands were called.
20
16
  class OrchestrationCommandNotSupported < ::RedisClient::Error
21
17
  def initialize(command)
22
18
  str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
23
- msg = "#{str} command should be used with care "\
24
- 'only by applications orchestrating Redis Cluster, like redis-cli, '\
25
- 'and the command if used out of the right context can leave the cluster '\
19
+ msg = "#{str} command should be used with care " \
20
+ 'only by applications orchestrating Redis Cluster, like redis-cli, ' \
21
+ 'and the command if used out of the right context can leave the cluster ' \
26
22
  'in a wrong state or cause data loss.'
27
23
  super(msg)
28
24
  end
29
25
  end
30
26
 
31
- # Raised when error occurs on any node of cluster.
32
27
  class ErrorCollection < ::RedisClient::Error
33
28
  attr_reader :errors
34
29
 
@@ -41,11 +36,10 @@ class RedisClient
41
36
 
42
37
  @errors = errors
43
38
  messages = @errors.map { |node_key, error| "#{node_key}: #{error.message}" }
44
- super("Command errors were replied on any node: #{messages.join(', ')}")
39
+ super("Errors occurred on any node: #{messages.join(', ')}")
45
40
  end
46
41
  end
47
42
 
48
- # Raised when cluster client can't select node.
49
43
  class AmbiguousNodeError < ::RedisClient::Error
50
44
  def initialize(command)
51
45
  super("Cluster client doesn't know which node the #{command} command should be sent to.")
@@ -55,8 +49,8 @@ class RedisClient
55
49
  class NodeMightBeDown < ::RedisClient::Error
56
50
  def initialize(_ = '')
57
51
  super(
58
- 'The client is trying to fetch the latest cluster state '\
59
- 'because a subset of nodes might be down. '\
52
+ 'The client is trying to fetch the latest cluster state ' \
53
+ 'because a subset of nodes might be down. ' \
60
54
  'It might continue to raise errors for a while.'
61
55
  )
62
56
  end
@@ -77,9 +77,8 @@ class RedisClient
77
77
  arr[8] = []
78
78
  next
79
79
  end
80
-
81
- arr[8] = arr[8].split(',').map { |r| r.split('-').map { |s| Integer(s) } }
82
- arr[8] = arr[8].map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
80
+ arr[8] = arr[8..].filter_map { |str| str.start_with?('[') ? nil : str.split('-').map { |s| Integer(s) } }
81
+ .map { |a| a.size == 1 ? a << a.first : a }.map(&:sort)
83
82
  end
84
83
 
85
84
  rows.map do |arr|
@@ -102,7 +101,7 @@ class RedisClient
102
101
  end
103
102
 
104
103
  def each(&block)
105
- @clients.values.each(&block)
104
+ @clients.each_value(&block)
106
105
  end
107
106
 
108
107
  def sample
@@ -181,7 +180,7 @@ class RedisClient
181
180
  def scale_reading_clients
182
181
  keys = replica_disabled? ? @replications.keys : @replications.values.map(&:first)
183
182
  @clients.select { |k, _| keys.include?(k) }.values.sort_by do |client|
184
- ::RedisClient::Cluster::NodeKey.build_from_host_port(client.config.host, client.config.port)
183
+ "#{client.config.host}-#{client.config.port}"
185
184
  end
186
185
  end
187
186
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client'
4
+ require 'redis_client/cluster/errors'
5
+
6
+ class RedisClient
7
+ class Cluster
8
+ class Pipeline
9
+ ReplySizeError = Class.new(::RedisClient::Error)
10
+
11
+ def initialize(router)
12
+ @router = router
13
+ @grouped = Hash.new([].freeze)
14
+ @size = 0
15
+ end
16
+
17
+ def call(*command, **kwargs)
18
+ node_key = @router.find_node_key(*command, primary_only: true)
19
+ @grouped[node_key] += [[@size, :call, command, kwargs]]
20
+ @size += 1
21
+ end
22
+
23
+ def call_once(*command, **kwargs)
24
+ node_key = @router.find_node_key(*command, primary_only: true)
25
+ @grouped[node_key] += [[@size, :call_once, command, kwargs]]
26
+ @size += 1
27
+ end
28
+
29
+ def blocking_call(timeout, *command, **kwargs)
30
+ node_key = @router.find_node_key(*command, primary_only: true)
31
+ @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
32
+ @size += 1
33
+ end
34
+
35
+ def empty?
36
+ @size.zero?
37
+ end
38
+
39
+ # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
40
+ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
41
+ all_replies = Array.new(@size)
42
+ errors = {}
43
+ threads = @grouped.map do |k, v|
44
+ Thread.new(@router, k, v) do |router, node_key, rows|
45
+ Thread.pass
46
+ replies = router.find_node(node_key).pipelined do |pipeline|
47
+ rows.each do |row|
48
+ case row[1]
49
+ when :call then pipeline.call(*row[2], **row[3])
50
+ when :call_once then pipeline.call_once(*row[2], **row[3])
51
+ when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
52
+ else raise NotImplementedError, row[1]
53
+ end
54
+ end
55
+ end
56
+
57
+ raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
58
+
59
+ rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
60
+ rescue StandardError => e
61
+ errors[node_key] = e
62
+ end
63
+ end
64
+
65
+ threads.each(&:join)
66
+ return all_replies if errors.empty?
67
+
68
+ raise ::RedisClient::Cluster::ErrorCollection, errors
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisClient
4
+ class Cluster
5
+ class PubSub
6
+ def initialize(router)
7
+ @router = router
8
+ @pubsub = nil
9
+ end
10
+
11
+ def call(*command, **kwargs)
12
+ close
13
+ @pubsub = @router.assign_node(*command).pubsub
14
+ @pubsub.call(*command, **kwargs)
15
+ end
16
+
17
+ def close
18
+ @pubsub&.close
19
+ @pubsub = nil
20
+ end
21
+
22
+ def next_event(timeout = nil)
23
+ @pubsub&.next_event(timeout)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,266 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis_client'
4
+ require 'redis_client/cluster/command'
5
+ require 'redis_client/cluster/errors'
6
+ require 'redis_client/cluster/key_slot_converter'
7
+ require 'redis_client/cluster/node'
8
+ require 'redis_client/cluster/node_key'
9
+
10
+ class RedisClient
11
+ class Cluster
12
+ class Router
13
+ ZERO_CURSOR_FOR_SCAN = '0'
14
+
15
+ attr_reader :node
16
+
17
+ def initialize(config, pool: nil, **kwargs)
18
+ @config = config.dup
19
+ @pool = pool
20
+ @client_kwargs = kwargs
21
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
22
+ @command = ::RedisClient::Cluster::Command.load(@node)
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
27
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
28
+
29
+ cmd = command.first.to_s.downcase
30
+ case cmd
31
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
32
+ @node.call_all(method, *args, **kwargs, &block).first
33
+ when 'flushall', 'flushdb'
34
+ @node.call_primaries(method, *args, **kwargs, &block).first
35
+ when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
36
+ when 'wait' then send_wait_command(method, *args, **kwargs, &block)
37
+ when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort_by(&:to_s)
38
+ when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
39
+ when 'scan' then scan(*command, **kwargs)
40
+ when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort_by(&:to_i)
41
+ when 'role' then @node.call_all(method, *args, **kwargs, &block)
42
+ when 'config' then send_config_command(method, *args, **kwargs, &block)
43
+ when 'client' then send_client_command(method, *args, **kwargs, &block)
44
+ when 'cluster' then send_cluster_command(method, *args, **kwargs, &block)
45
+ when 'readonly', 'readwrite', 'shutdown'
46
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
47
+ when 'memory' then send_memory_command(method, *args, **kwargs, &block)
48
+ when 'script' then send_script_command(method, *args, **kwargs, &block)
49
+ when 'pubsub' then send_pubsub_command(method, *args, **kwargs, &block)
50
+ when 'discard', 'exec', 'multi', 'unwatch'
51
+ raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
52
+ else
53
+ node = assign_node(*command)
54
+ try_send(node, method, *args, **kwargs, &block)
55
+ end
56
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
57
+ update_cluster_info!
58
+ raise ::RedisClient::Cluster::NodeMightBeDown
59
+ rescue ::RedisClient::Cluster::ErrorCollection => e
60
+ update_cluster_info! if e.errors.values.any? do |err|
61
+ err.message.start_with?('CLUSTERDOWN Hash slot not served')
62
+ end
63
+ raise
64
+ end
65
+
66
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
67
+ # Redirection and resharding
68
+ def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
69
+ node.send(method, *args, **kwargs, &block)
70
+ rescue ::RedisClient::CommandError => e
71
+ raise if retry_count <= 0
72
+
73
+ if e.message.start_with?('MOVED')
74
+ node = assign_redirection_node(e.message)
75
+ retry_count -= 1
76
+ retry
77
+ elsif e.message.start_with?('ASK')
78
+ node = assign_asking_node(e.message)
79
+ node.call('ASKING')
80
+ retry_count -= 1
81
+ retry
82
+ elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
83
+ update_cluster_info!
84
+ retry_count -= 1
85
+ retry
86
+ else
87
+ raise
88
+ end
89
+ rescue ::RedisClient::ConnectionError
90
+ raise if retry_count <= 0
91
+
92
+ update_cluster_info!
93
+ retry_count -= 1
94
+ retry
95
+ end
96
+
97
+ def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
98
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
99
+ input_cursor = Integer(command[1])
100
+
101
+ client_index = input_cursor % 256
102
+ raw_cursor = input_cursor >> 8
103
+
104
+ clients = @node.scale_reading_clients
105
+
106
+ client = clients[client_index]
107
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
108
+
109
+ command[1] = raw_cursor.to_s
110
+
111
+ result_cursor, result_keys = client.call(*command, **kwargs)
112
+ result_cursor = Integer(result_cursor)
113
+
114
+ client_index += 1 if result_cursor == 0
115
+
116
+ [((result_cursor << 8) + client_index).to_s, result_keys]
117
+ end
118
+
119
+ def assign_node(*command)
120
+ node_key = find_node_key(*command)
121
+ find_node(node_key)
122
+ end
123
+
124
+ def find_node_key(*command, primary_only: false)
125
+ key = @command.extract_first_key(command)
126
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
127
+
128
+ if @command.should_send_to_primary?(command) || primary_only
129
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
130
+ else
131
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
132
+ end
133
+ end
134
+
135
+ def find_node(node_key, retry_count: 3)
136
+ @node.find_by(node_key)
137
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
138
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
139
+
140
+ update_cluster_info!
141
+ retry_count -= 1
142
+ retry
143
+ end
144
+
145
+ private
146
+
147
+ def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
148
+ @node.call_primaries(method, *args, **kwargs, &block).select { |r| r.is_a?(Integer) }.sum
149
+ rescue ::RedisClient::Cluster::ErrorCollection => e
150
+ raise if retry_count <= 0
151
+ raise if e.errors.values.none? do |err|
152
+ err.message.include?('WAIT cannot be used with replica instances')
153
+ end
154
+
155
+ update_cluster_info!
156
+ retry_count -= 1
157
+ retry
158
+ end
159
+
160
+ def send_config_command(method, *args, **kwargs, &block)
161
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
162
+
163
+ case command[1].to_s.downcase
164
+ when 'resetstat', 'rewrite', 'set'
165
+ @node.call_all(method, *args, **kwargs, &block).first
166
+ else assign_node(*command).send(method, *args, **kwargs, &block)
167
+ end
168
+ end
169
+
170
+ def send_memory_command(method, *args, **kwargs, &block)
171
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
172
+
173
+ case command[1].to_s.downcase
174
+ when 'stats' then @node.call_all(method, *args, **kwargs, &block)
175
+ when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
176
+ else assign_node(*command).send(method, *args, **kwargs, &block)
177
+ end
178
+ end
179
+
180
+ def send_client_command(method, *args, **kwargs, &block)
181
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
182
+
183
+ case command[1].to_s.downcase
184
+ when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
185
+ when 'pause', 'reply', 'setname'
186
+ @node.call_all(method, *args, **kwargs, &block).first
187
+ else assign_node(*command).send(method, *args, **kwargs, &block)
188
+ end
189
+ end
190
+
191
+ def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
192
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
193
+ subcommand = command[1].to_s.downcase
194
+
195
+ case subcommand
196
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
197
+ 'reset', 'set-config-epoch', 'setslot'
198
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
199
+ when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
200
+ when 'getkeysinslot'
201
+ raise ArgumentError, command.join(' ') if command.size != 4
202
+
203
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
204
+ else assign_node(*command).send(method, *args, **kwargs, &block)
205
+ end
206
+ end
207
+
208
+ def send_script_command(method, *args, **kwargs, &block)
209
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
210
+
211
+ case command[1].to_s.downcase
212
+ when 'debug', 'kill'
213
+ @node.call_all(method, *args, **kwargs, &block).first
214
+ when 'flush', 'load'
215
+ @node.call_primaries(method, *args, **kwargs, &block).first
216
+ else assign_node(*command).send(method, *args, **kwargs, &block)
217
+ end
218
+ end
219
+
220
+ def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
221
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
222
+
223
+ case command[1].to_s.downcase
224
+ when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort_by(&:to_s)
225
+ when 'numsub'
226
+ @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
227
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
228
+ when 'numpat' then @node.call_all(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
229
+ else assign_node(*command).send(method, *args, **kwargs, &block)
230
+ end
231
+ end
232
+
233
+ def assign_redirection_node(err_msg)
234
+ _, slot, node_key = err_msg.split
235
+ slot = slot.to_i
236
+ @node.update_slot(slot, node_key)
237
+ find_node(node_key)
238
+ end
239
+
240
+ def assign_asking_node(err_msg)
241
+ _, _, node_key = err_msg.split
242
+ find_node(node_key)
243
+ end
244
+
245
+ def fetch_cluster_info(config, pool: nil, **kwargs)
246
+ node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
247
+ node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
248
+ config.update_node(node_addrs)
249
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
250
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
251
+ end
252
+
253
+ def update_cluster_info!
254
+ @mutex.synchronize do
255
+ begin
256
+ @node.call_all(:close)
257
+ rescue ::RedisClient::Cluster::ErrorCollection
258
+ # ignore
259
+ end
260
+
261
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
262
+ end
263
+ end
264
+ end
265
+ end
266
+ end
@@ -1,133 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'redis_client'
4
- require 'redis_client/cluster/command'
5
- require 'redis_client/cluster/errors'
6
- require 'redis_client/cluster/key_slot_converter'
7
- require 'redis_client/cluster/node'
8
- require 'redis_client/cluster/node_key'
4
+ require 'redis_client/cluster/pipeline'
5
+ require 'redis_client/cluster/pub_sub'
6
+ require 'redis_client/cluster/router'
9
7
 
10
8
  class RedisClient
11
9
  class Cluster
12
- class Pipeline
13
- ReplySizeError = Class.new(::RedisClient::Error)
14
-
15
- def initialize(client)
16
- @client = client
17
- @grouped = Hash.new([].freeze)
18
- @size = 0
19
- end
20
-
21
- def call(*command, **kwargs)
22
- node_key = @client.send(:find_node_key, *command, primary_only: true)
23
- @grouped[node_key] += [[@size, :call, command, kwargs]]
24
- @size += 1
25
- end
26
-
27
- def call_once(*command, **kwargs)
28
- node_key = @client.send(:find_node_key, *command, primary_only: true)
29
- @grouped[node_key] += [[@size, :call_once, command, kwargs]]
30
- @size += 1
31
- end
32
-
33
- def blocking_call(timeout, *command, **kwargs)
34
- node_key = @client.send(:find_node_key, *command, primary_only: true)
35
- @grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
36
- @size += 1
37
- end
38
-
39
- def empty?
40
- @size.zero?
41
- end
42
-
43
- # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
44
- def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
45
- all_replies = Array.new(@size)
46
- errors = {}
47
- threads = @grouped.map do |k, v|
48
- Thread.new(@client, k, v) do |client, node_key, rows|
49
- Thread.pass
50
- replies = client.send(:find_node, node_key).pipelined do |pipeline|
51
- rows.each do |row|
52
- case row[1]
53
- when :call then pipeline.call(*row[2], **row[3])
54
- when :call_once then pipeline.call_once(*row[2], **row[3])
55
- when :blocking_call then pipeline.blocking_call(row[2], *row[3], **row[4])
56
- else raise NotImplementedError, row[1]
57
- end
58
- end
59
- end
60
-
61
- raise ReplySizeError, "commands: #{rows.size}, replies: #{replies.size}" if rows.size != replies.size
62
-
63
- rows.each_with_index { |row, idx| all_replies[row.first] = replies[idx] }
64
- rescue StandardError => e
65
- errors[node_key] = e
66
- end
67
- end
68
-
69
- threads.each(&:join)
70
- return all_replies if errors.empty?
71
-
72
- raise ::RedisClient::Cluster::ErrorCollection, errors
73
- end
74
- end
75
-
76
- class PubSub
77
- def initialize(client)
78
- @client = client
79
- @pubsub = nil
80
- end
81
-
82
- def call(*command, **kwargs)
83
- close
84
- @pubsub = @client.send(:assign_node, *command).pubsub
85
- @pubsub.call(*command, **kwargs)
86
- end
87
-
88
- def close
89
- @pubsub&.close
90
- @pubsub = nil
91
- end
92
-
93
- def next_event(timeout = nil)
94
- @pubsub&.next_event(timeout)
95
- end
96
- end
97
-
98
10
  ZERO_CURSOR_FOR_SCAN = '0'
99
- CMD_SCAN = 'SCAN'
100
- CMD_SSCAN = 'SSCAN'
101
- CMD_HSCAN = 'HSCAN'
102
- CMD_ZSCAN = 'ZSCAN'
103
- CMD_ASKING = 'ASKING'
104
- REPLY_OK = 'OK'
105
- REPLY_MOVED = 'MOVED'
106
- REPLY_ASK = 'ASK'
107
11
 
108
12
  def initialize(config, pool: nil, **kwargs)
109
- @config = config.dup
110
- @pool = pool
111
- @client_kwargs = kwargs
112
- @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
113
- @command = ::RedisClient::Cluster::Command.load(@node)
114
- @mutex = Mutex.new
13
+ @router = ::RedisClient::Cluster::Router.new(config, pool: pool, **kwargs)
115
14
  end
116
15
 
117
16
  def inspect
118
- "#<#{self.class.name} #{@node.node_keys.join(', ')}>"
17
+ "#<#{self.class.name} #{@router.node.node_keys.join(', ')}>"
119
18
  end
120
19
 
121
20
  def call(*command, **kwargs)
122
- send_command(:call, *command, **kwargs)
21
+ @router.send_command(:call, *command, **kwargs)
123
22
  end
124
23
 
125
24
  def call_once(*command, **kwargs)
126
- send_command(:call_once, *command, **kwargs)
25
+ @router.send_command(:call_once, *command, **kwargs)
127
26
  end
128
27
 
129
28
  def blocking_call(timeout, *command, **kwargs)
130
- send_command(:blocking_call, timeout, *command, **kwargs)
29
+ @router.send_command(:blocking_call, timeout, *command, **kwargs)
131
30
  end
132
31
 
133
32
  def scan(*args, **kwargs, &block)
@@ -135,29 +34,29 @@ class RedisClient
135
34
 
136
35
  cursor = ZERO_CURSOR_FOR_SCAN
137
36
  loop do
138
- cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
37
+ cursor, keys = @router.scan('SCAN', cursor, *args, **kwargs)
139
38
  keys.each(&block)
140
39
  break if cursor == ZERO_CURSOR_FOR_SCAN
141
40
  end
142
41
  end
143
42
 
144
43
  def sscan(key, *args, **kwargs, &block)
145
- node = assign_node(CMD_SSCAN, key)
146
- try_send(node, :sscan, key, *args, **kwargs, &block)
44
+ node = @router.assign_node('SSCAN', key)
45
+ @router.try_send(node, :sscan, key, *args, **kwargs, &block)
147
46
  end
148
47
 
149
48
  def hscan(key, *args, **kwargs, &block)
150
- node = assign_node(CMD_HSCAN, key)
151
- try_send(node, :hscan, key, *args, **kwargs, &block)
49
+ node = @router.assign_node('HSCAN', key)
50
+ @router.try_send(node, :hscan, key, *args, **kwargs, &block)
152
51
  end
153
52
 
154
53
  def zscan(key, *args, **kwargs, &block)
155
- node = assign_node(CMD_ZSCAN, key)
156
- try_send(node, :zscan, key, *args, **kwargs, &block)
54
+ node = @router.assign_node('ZSCAN', key)
55
+ @router.try_send(node, :zscan, key, *args, **kwargs, &block)
157
56
  end
158
57
 
159
58
  def pipelined
160
- pipeline = ::RedisClient::Cluster::Pipeline.new(self)
59
+ pipeline = ::RedisClient::Cluster::Pipeline.new(@router)
161
60
  yield pipeline
162
61
  return [] if pipeline.empty? == 0
163
62
 
@@ -165,239 +64,12 @@ class RedisClient
165
64
  end
166
65
 
167
66
  def pubsub
168
- ::RedisClient::Cluster::PubSub.new(self)
67
+ ::RedisClient::Cluster::PubSub.new(@router)
169
68
  end
170
69
 
171
70
  def close
172
- @node.call_all(:close)
71
+ @router.node.call_all(:close)
173
72
  nil
174
- rescue StandardError
175
- # ignore
176
- end
177
-
178
- private
179
-
180
- def fetch_cluster_info!(config, pool: nil, **kwargs)
181
- node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
182
- node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
183
- config.update_node(node_addrs)
184
- ::RedisClient::Cluster::Node.new(config.per_node_key,
185
- node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
186
- end
187
-
188
- def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
189
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
190
-
191
- cmd = command.first.to_s.downcase
192
- case cmd
193
- when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
194
- @node.call_all(method, *args, **kwargs, &block).first
195
- when 'flushall', 'flushdb'
196
- @node.call_primaries(method, *args, **kwargs, &block).first
197
- when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
198
- when 'wait' then send_wait_command(method, *args, **kwargs, &block)
199
- when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort
200
- when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).sum
201
- when 'scan' then _scan(*command, **kwargs)
202
- when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort
203
- when 'role' then @node.call_all(method, *args, **kwargs, &block)
204
- when 'config' then send_config_command(method, *args, **kwargs, &block)
205
- when 'client' then send_client_command(method, *args, **kwargs, &block)
206
- when 'cluster' then send_cluster_command(method, *args, **kwargs, &block)
207
- when 'readonly', 'readwrite', 'shutdown'
208
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
209
- when 'memory' then send_memory_command(method, *args, **kwargs, &block)
210
- when 'script' then send_script_command(method, *args, **kwargs, &block)
211
- when 'pubsub' then send_pubsub_command(method, *args, **kwargs, &block)
212
- when 'discard', 'exec', 'multi', 'unwatch'
213
- raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
214
- else
215
- node = assign_node(*command)
216
- try_send(node, method, *args, **kwargs, &block)
217
- end
218
- rescue RedisClient::Cluster::Node::ReloadNeeded
219
- update_cluster_info!
220
- raise ::RedisClient::Cluster::NodeMightBeDown
221
- end
222
-
223
- def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
224
- @node.call_primaries(method, *args, **kwargs, &block).sum
225
- rescue RedisClient::Cluster::ErrorCollection => e
226
- raise if retry_count <= 0
227
- raise if e.errors.values.none? do |err|
228
- err.message.include?('WAIT cannot be used with replica instances')
229
- end
230
-
231
- update_cluster_info!
232
- retry_count -= 1
233
- retry
234
- end
235
-
236
- def send_config_command(method, *args, **kwargs, &block)
237
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
238
-
239
- case command[1].to_s.downcase
240
- when 'resetstat', 'rewrite', 'set'
241
- @node.call_all(method, *args, **kwargs, &block).first
242
- else assign_node(*command).send(method, *args, **kwargs, &block)
243
- end
244
- end
245
-
246
- def send_memory_command(method, *args, **kwargs, &block)
247
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
248
-
249
- case command[1].to_s.downcase
250
- when 'stats' then @node.call_all(method, *args, **kwargs, &block)
251
- when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
252
- else assign_node(*command).send(method, *args, **kwargs, &block)
253
- end
254
- end
255
-
256
- def send_client_command(method, *args, **kwargs, &block)
257
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
258
-
259
- case command[1].to_s.downcase
260
- when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
261
- when 'pause', 'reply', 'setname'
262
- @node.call_all(method, *args, **kwargs, &block).first
263
- else assign_node(*command).send(method, *args, **kwargs, &block)
264
- end
265
- end
266
-
267
- def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
268
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
269
- subcommand = command[1].to_s.downcase
270
-
271
- case subcommand
272
- when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
273
- 'reset', 'set-config-epoch', 'setslot'
274
- raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
275
- when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
276
- when 'getkeysinslot'
277
- raise ArgumentError, command.join(' ') if command.size != 4
278
-
279
- find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
280
- else assign_node(*command).send(method, *args, **kwargs, &block)
281
- end
282
- end
283
-
284
- def send_script_command(method, *args, **kwargs, &block)
285
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
286
-
287
- case command[1].to_s.downcase
288
- when 'debug', 'kill'
289
- @node.call_all(method, *args, **kwargs, &block).first
290
- when 'flush', 'load'
291
- @node.call_primaries(method, *args, **kwargs, &block).first
292
- else assign_node(*command).send(method, *args, **kwargs, &block)
293
- end
294
- end
295
-
296
- def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
297
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
298
-
299
- case command[1].to_s.downcase
300
- when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort
301
- when 'numsub'
302
- @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
303
- .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
304
- when 'numpat' then @node.call_all(method, *args, **kwargs, &block).sum
305
- else assign_node(*command).send(method, *args, **kwargs, &block)
306
- end
307
- end
308
-
309
- # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
310
- # Redirection and resharding
311
- def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
312
- node.send(method, *args, **kwargs, &block)
313
- rescue ::RedisClient::CommandError => e
314
- raise if retry_count <= 0
315
-
316
- if e.message.start_with?(REPLY_MOVED)
317
- node = assign_redirection_node(e.message)
318
- retry_count -= 1
319
- retry
320
- elsif e.message.start_with?(REPLY_ASK)
321
- node = assign_asking_node(e.message)
322
- node.call(CMD_ASKING)
323
- retry_count -= 1
324
- retry
325
- else
326
- raise
327
- end
328
- rescue ::RedisClient::ConnectionError
329
- raise if retry_count <= 0
330
-
331
- update_cluster_info!
332
- retry_count -= 1
333
- retry
334
- end
335
-
336
- def _scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
337
- command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
338
- input_cursor = Integer(command[1])
339
-
340
- client_index = input_cursor % 256
341
- raw_cursor = input_cursor >> 8
342
-
343
- clients = @node.scale_reading_clients
344
-
345
- client = clients[client_index]
346
- return [ZERO_CURSOR_FOR_SCAN, []] unless client
347
-
348
- command[1] = raw_cursor.to_s
349
-
350
- result_cursor, result_keys = client.call(*command, **kwargs)
351
- result_cursor = Integer(result_cursor)
352
-
353
- client_index += 1 if result_cursor == 0
354
-
355
- [((result_cursor << 8) + client_index).to_s, result_keys]
356
- end
357
-
358
- def assign_redirection_node(err_msg)
359
- _, slot, node_key = err_msg.split
360
- slot = slot.to_i
361
- @node.update_slot(slot, node_key)
362
- find_node(node_key)
363
- end
364
-
365
- def assign_asking_node(err_msg)
366
- _, _, node_key = err_msg.split
367
- find_node(node_key)
368
- end
369
-
370
- def assign_node(*command)
371
- node_key = find_node_key(*command)
372
- find_node(node_key)
373
- end
374
-
375
- def find_node_key(*command, primary_only: false)
376
- key = @command.extract_first_key(command)
377
- slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
378
-
379
- if @command.should_send_to_primary?(command) || primary_only
380
- @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
381
- else
382
- @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
383
- end
384
- end
385
-
386
- def find_node(node_key, retry_count: 3)
387
- @node.find_by(node_key)
388
- rescue ::RedisClient::Cluster::Node::ReloadNeeded
389
- raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
390
-
391
- update_cluster_info!
392
- retry_count -= 1
393
- retry
394
- end
395
-
396
- def update_cluster_info!
397
- @mutex.synchronize do
398
- close
399
- @node = fetch_cluster_info!(@config, pool: @pool, **@client_kwargs)
400
- end
401
73
  end
402
74
  end
403
75
  end
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.0.9
4
+ version: 0.0.12
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-06-22 00:00:00.000000000 Z
11
+ date: 2022-07-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis-client
@@ -37,6 +37,9 @@ files:
37
37
  - lib/redis_client/cluster/key_slot_converter.rb
38
38
  - lib/redis_client/cluster/node.rb
39
39
  - lib/redis_client/cluster/node_key.rb
40
+ - lib/redis_client/cluster/pipeline.rb
41
+ - lib/redis_client/cluster/pub_sub.rb
42
+ - lib/redis_client/cluster/router.rb
40
43
  - lib/redis_client/cluster_config.rb
41
44
  - lib/redis_cluster_client.rb
42
45
  homepage: https://github.com/redis-rb/redis-cluster-client