redis-cluster-client 0.0.9 → 0.0.12

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: 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