redis-cluster-client 0.0.10 → 0.0.11

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