redis-cluster-client 0.0.10 → 0.0.11

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: 86d73675660702eb937b5d27925248d16b64fe46306d49da0c2b89ee49b5dc34
4
- data.tar.gz: d2472791161a5ba746cc29c504353dc0e8414c093a531ff1017653a5a354d7dd
3
+ metadata.gz: d7c3e9a3b764d9e5fc52670d17c175ee9ccda2dcd56b4a977b0e9a1e0b71d972
4
+ data.tar.gz: 191e1fe163f6f2fd111e4b2f69638aa28462d74e002f1ff0a11fc0f52cc740cd
5
5
  SHA512:
6
- metadata.gz: bd12c8ebbc013d36ead0ee5bb4f8914ad8e5dec4a302e1aa54890ba36abd2358376bc0329e94b859c9e5c5d71d1a07d11a2f9684db655fef375000e792ca5f0c
7
- data.tar.gz: d05782f64503cc104ef789683448d967ca6d71355cdc8a1411e8a92ea98f9412ad94a7a411bc0a93ffffb5ff384f6d3fb6e48ef83cc07bf0fe907872045e2567
6
+ metadata.gz: a42c6a871302686e33026ed570575d5b51637b707501609fdf9e574a5a976e06af388bc69bebd5bb22a58a30741c4b3e43d883d154782675446ce88c49ac2b37
7
+ data.tar.gz: e2ae0a6dd3af75407cc4d795492afff75e5bc7c01fee84be2ef0761e67dda0d9f93cc103b99219bd1b5e8df9dc3ff1dc86b3406c3a23dc1d011b649c363ab163
@@ -81,7 +81,7 @@ class RedisClient
81
81
  when 'memory'
82
82
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
83
83
  when 'migrate'
84
- command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
84
+ command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
85
85
  when 'xread', 'xreadgroup'
86
86
  determine_optional_key_position(command, 'streams')
87
87
  else
@@ -16,9 +16,9 @@ class RedisClient
16
16
  class OrchestrationCommandNotSupported < ::RedisClient::Error
17
17
  def initialize(command)
18
18
  str = ERR_ARG_NORMALIZATION.call(command).map(&:to_s).join(' ').upcase
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 '\
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 ' \
22
22
  'in a wrong state or cause data loss.'
23
23
  super(msg)
24
24
  end
@@ -49,8 +49,8 @@ class RedisClient
49
49
  class NodeMightBeDown < ::RedisClient::Error
50
50
  def initialize(_ = '')
51
51
  super(
52
- 'The client is trying to fetch the latest cluster state '\
53
- '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. ' \
54
54
  'It might continue to raise errors for a while.'
55
55
  )
56
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|
@@ -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,257 @@
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
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
38
+ when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).sum
39
+ when 'scan' then scan(*command, **kwargs)
40
+ when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort
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
+ end
60
+
61
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
62
+ # Redirection and resharding
63
+ def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
64
+ node.send(method, *args, **kwargs, &block)
65
+ rescue ::RedisClient::CommandError => e
66
+ raise if retry_count <= 0
67
+
68
+ if e.message.start_with?('MOVED')
69
+ node = assign_redirection_node(e.message)
70
+ retry_count -= 1
71
+ retry
72
+ elsif e.message.start_with?('ASK')
73
+ node = assign_asking_node(e.message)
74
+ node.call('ASKING')
75
+ retry_count -= 1
76
+ retry
77
+ else
78
+ raise
79
+ end
80
+ rescue ::RedisClient::ConnectionError
81
+ raise if retry_count <= 0
82
+
83
+ update_cluster_info!
84
+ retry_count -= 1
85
+ retry
86
+ end
87
+
88
+ def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
89
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
90
+ input_cursor = Integer(command[1])
91
+
92
+ client_index = input_cursor % 256
93
+ raw_cursor = input_cursor >> 8
94
+
95
+ clients = @node.scale_reading_clients
96
+
97
+ client = clients[client_index]
98
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
99
+
100
+ command[1] = raw_cursor.to_s
101
+
102
+ result_cursor, result_keys = client.call(*command, **kwargs)
103
+ result_cursor = Integer(result_cursor)
104
+
105
+ client_index += 1 if result_cursor == 0
106
+
107
+ [((result_cursor << 8) + client_index).to_s, result_keys]
108
+ end
109
+
110
+ def assign_node(*command)
111
+ node_key = find_node_key(*command)
112
+ find_node(node_key)
113
+ end
114
+
115
+ def find_node_key(*command, primary_only: false)
116
+ key = @command.extract_first_key(command)
117
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
118
+
119
+ if @command.should_send_to_primary?(command) || primary_only
120
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
121
+ else
122
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
123
+ end
124
+ end
125
+
126
+ def find_node(node_key, retry_count: 3)
127
+ @node.find_by(node_key)
128
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
129
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
130
+
131
+ update_cluster_info!
132
+ retry_count -= 1
133
+ retry
134
+ end
135
+
136
+ private
137
+
138
+ def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
139
+ @node.call_primaries(method, *args, **kwargs, &block).select { |r| r.is_a?(Integer) }.sum
140
+ rescue ::RedisClient::Cluster::ErrorCollection => e
141
+ raise if retry_count <= 0
142
+ raise if e.errors.values.none? do |err|
143
+ err.message.include?('WAIT cannot be used with replica instances')
144
+ end
145
+
146
+ update_cluster_info!
147
+ retry_count -= 1
148
+ retry
149
+ end
150
+
151
+ def send_config_command(method, *args, **kwargs, &block)
152
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
153
+
154
+ case command[1].to_s.downcase
155
+ when 'resetstat', 'rewrite', 'set'
156
+ @node.call_all(method, *args, **kwargs, &block).first
157
+ else assign_node(*command).send(method, *args, **kwargs, &block)
158
+ end
159
+ end
160
+
161
+ def send_memory_command(method, *args, **kwargs, &block)
162
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
163
+
164
+ case command[1].to_s.downcase
165
+ when 'stats' then @node.call_all(method, *args, **kwargs, &block)
166
+ when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
167
+ else assign_node(*command).send(method, *args, **kwargs, &block)
168
+ end
169
+ end
170
+
171
+ def send_client_command(method, *args, **kwargs, &block)
172
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
173
+
174
+ case command[1].to_s.downcase
175
+ when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
176
+ when 'pause', 'reply', 'setname'
177
+ @node.call_all(method, *args, **kwargs, &block).first
178
+ else assign_node(*command).send(method, *args, **kwargs, &block)
179
+ end
180
+ end
181
+
182
+ def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
183
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
184
+ subcommand = command[1].to_s.downcase
185
+
186
+ case subcommand
187
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
188
+ 'reset', 'set-config-epoch', 'setslot'
189
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
190
+ when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
191
+ when 'getkeysinslot'
192
+ raise ArgumentError, command.join(' ') if command.size != 4
193
+
194
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
195
+ else assign_node(*command).send(method, *args, **kwargs, &block)
196
+ end
197
+ end
198
+
199
+ def send_script_command(method, *args, **kwargs, &block)
200
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
201
+
202
+ case command[1].to_s.downcase
203
+ when 'debug', 'kill'
204
+ @node.call_all(method, *args, **kwargs, &block).first
205
+ when 'flush', 'load'
206
+ @node.call_primaries(method, *args, **kwargs, &block).first
207
+ else assign_node(*command).send(method, *args, **kwargs, &block)
208
+ end
209
+ end
210
+
211
+ def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
212
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
213
+
214
+ case command[1].to_s.downcase
215
+ when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort
216
+ when 'numsub'
217
+ @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
218
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
219
+ when 'numpat' then @node.call_all(method, *args, **kwargs, &block).sum
220
+ else assign_node(*command).send(method, *args, **kwargs, &block)
221
+ end
222
+ end
223
+
224
+ def assign_redirection_node(err_msg)
225
+ _, slot, node_key = err_msg.split
226
+ slot = slot.to_i
227
+ @node.update_slot(slot, node_key)
228
+ find_node(node_key)
229
+ end
230
+
231
+ def assign_asking_node(err_msg)
232
+ _, _, node_key = err_msg.split
233
+ find_node(node_key)
234
+ end
235
+
236
+ def fetch_cluster_info(config, pool: nil, **kwargs)
237
+ node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
238
+ node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
239
+ config.update_node(node_addrs)
240
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
241
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
242
+ end
243
+
244
+ def update_cluster_info!
245
+ @mutex.synchronize do
246
+ begin
247
+ @node.call_all(:close)
248
+ rescue ::RedisClient::Cluster::ErrorCollection
249
+ # ignore
250
+ end
251
+
252
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
253
+ end
254
+ end
255
+ end
256
+ end
257
+ 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.10
4
+ version: 0.0.11
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-06-27 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