redis-cluster-client 0.0.10 → 0.1.0

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: 367df4e02bcd425f14da73b2ff243dfdb7fd44c6cb565bcc46952b939057904f
4
+ data.tar.gz: bfdb8033d0695fe0389563ad563c8445f9624e99ef6315e7374ee7f2e440fe40
5
5
  SHA512:
6
- metadata.gz: bd12c8ebbc013d36ead0ee5bb4f8914ad8e5dec4a302e1aa54890ba36abd2358376bc0329e94b859c9e5c5d71d1a07d11a2f9684db655fef375000e792ca5f0c
7
- data.tar.gz: d05782f64503cc104ef789683448d967ca6d71355cdc8a1411e8a92ea98f9412ad94a7a411bc0a93ffffb5ff384f6d3fb6e48ef83cc07bf0fe907872045e2567
6
+ metadata.gz: 278740e8829a1def72a411552ed7c17d0d265930c81dfc2b967cfa325a74cdbdbd0adb832dacc77e4c86c8316648695b94044cb6fdf3dd3267617d5f18b95ec1
7
+ data.tar.gz: 007bf587198b7575dabee128c0500700ef0d10c7b699a632023ce4d80fa788d2a88e851748f041ab5f6453350d7de6f9d8c96bcbc798deec7acbf37b2700ea6a
@@ -55,6 +55,10 @@ class RedisClient
55
55
  dig_details(command, :readonly)
56
56
  end
57
57
 
58
+ def exists?(name)
59
+ @details.key?(name.to_s.downcase)
60
+ end
61
+
58
62
  private
59
63
 
60
64
  def pick_details(details)
@@ -81,7 +85,7 @@ class RedisClient
81
85
  when 'memory'
82
86
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
83
87
  when 'migrate'
84
- command[3] == '""' ? determine_optional_key_position(command, 'keys') : 3
88
+ command[3].empty? ? determine_optional_key_position(command, 'keys') : 3
85
89
  when 'xread', 'xreadgroup'
86
90
  determine_optional_key_position(command, 'streams')
87
91
  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, **kwargs)
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, **kwargs)
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, **kwargs)
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, **kwargs).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,282 @@
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
+ @command_builder = @config.command_builder
24
+ @mutex = Mutex.new
25
+ end
26
+
27
+ def send_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
28
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
29
+ command = @command_builder.generate!(command, kwargs)
30
+
31
+ cmd = command.first.to_s.downcase
32
+ case cmd
33
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
34
+ @node.call_all(method, *args, **kwargs, &block).first
35
+ when 'flushall', 'flushdb'
36
+ @node.call_primaries(method, *args, **kwargs, &block).first
37
+ when 'ping' then @node.send_ping(method, *args, **kwargs, &block).first
38
+ when 'wait' then send_wait_command(method, *args, **kwargs, &block)
39
+ when 'keys' then @node.call_replicas(method, *args, **kwargs, &block).flatten.sort_by(&:to_s)
40
+ when 'dbsize' then @node.call_replicas(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
41
+ when 'scan' then scan(*command, **kwargs)
42
+ when 'lastsave' then @node.call_all(method, *args, **kwargs, &block).sort_by(&:to_i)
43
+ when 'role' then @node.call_all(method, *args, **kwargs, &block)
44
+ when 'config' then send_config_command(method, *args, **kwargs, &block)
45
+ when 'client' then send_client_command(method, *args, **kwargs, &block)
46
+ when 'cluster' then send_cluster_command(method, *args, **kwargs, &block)
47
+ when 'readonly', 'readwrite', 'shutdown'
48
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, cmd
49
+ when 'memory' then send_memory_command(method, *args, **kwargs, &block)
50
+ when 'script' then send_script_command(method, *args, **kwargs, &block)
51
+ when 'pubsub' then send_pubsub_command(method, *args, **kwargs, &block)
52
+ when 'discard', 'exec', 'multi', 'unwatch'
53
+ raise ::RedisClient::Cluster::AmbiguousNodeError, cmd
54
+ else
55
+ node = assign_node(*command)
56
+ try_send(node, method, *args, **kwargs, &block)
57
+ end
58
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
59
+ update_cluster_info!
60
+ raise ::RedisClient::Cluster::NodeMightBeDown
61
+ rescue ::RedisClient::Cluster::ErrorCollection => e
62
+ update_cluster_info! if e.errors.values.any? do |err|
63
+ err.message.start_with?('CLUSTERDOWN Hash slot not served')
64
+ end
65
+ raise
66
+ end
67
+
68
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
69
+ # Redirection and resharding
70
+ def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
71
+ node.send(method, *args, **kwargs, &block)
72
+ rescue ::RedisClient::CommandError => e
73
+ raise if retry_count <= 0
74
+
75
+ if e.message.start_with?('MOVED')
76
+ node = assign_redirection_node(e.message)
77
+ retry_count -= 1
78
+ retry
79
+ elsif e.message.start_with?('ASK')
80
+ node = assign_asking_node(e.message)
81
+ node.call('ASKING')
82
+ retry_count -= 1
83
+ retry
84
+ elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
85
+ update_cluster_info!
86
+ retry_count -= 1
87
+ retry
88
+ else
89
+ raise
90
+ end
91
+ rescue ::RedisClient::ConnectionError
92
+ raise if retry_count <= 0
93
+
94
+ update_cluster_info!
95
+ retry_count -= 1
96
+ retry
97
+ end
98
+
99
+ def scan(*command, **kwargs) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
100
+ command = @command_builder.generate!(command, kwargs)
101
+
102
+ command[1] = ZERO_CURSOR_FOR_SCAN if command.size == 1
103
+ input_cursor = Integer(command[1])
104
+
105
+ client_index = input_cursor % 256
106
+ raw_cursor = input_cursor >> 8
107
+
108
+ clients = @node.scale_reading_clients
109
+
110
+ client = clients[client_index]
111
+ return [ZERO_CURSOR_FOR_SCAN, []] unless client
112
+
113
+ command[1] = raw_cursor.to_s
114
+
115
+ result_cursor, result_keys = client.call(*command, **kwargs)
116
+ result_cursor = Integer(result_cursor)
117
+
118
+ client_index += 1 if result_cursor == 0
119
+
120
+ [((result_cursor << 8) + client_index).to_s, result_keys]
121
+ end
122
+
123
+ def assign_node(*command, **kwargs)
124
+ node_key = find_node_key(*command, **kwargs)
125
+ find_node(node_key)
126
+ end
127
+
128
+ def find_node_key(*command, primary_only: false, **kwargs)
129
+ command = @command_builder.generate!(command, kwargs)
130
+
131
+ key = @command.extract_first_key(command)
132
+ slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
133
+
134
+ if @command.should_send_to_primary?(command) || primary_only
135
+ @node.find_node_key_of_primary(slot) || @node.primary_node_keys.sample
136
+ else
137
+ @node.find_node_key_of_replica(slot) || @node.replica_node_keys.sample
138
+ end
139
+ end
140
+
141
+ def find_node(node_key, retry_count: 3)
142
+ @node.find_by(node_key)
143
+ rescue ::RedisClient::Cluster::Node::ReloadNeeded
144
+ raise ::RedieClient::Cluster::NodeMightBeDown if retry_count <= 0
145
+
146
+ update_cluster_info!
147
+ retry_count -= 1
148
+ retry
149
+ end
150
+
151
+ def command_exists?(name)
152
+ @command.exists?(name)
153
+ end
154
+
155
+ private
156
+
157
+ def send_wait_command(method, *args, retry_count: 3, **kwargs, &block)
158
+ @node.call_primaries(method, *args, **kwargs, &block).select { |r| r.is_a?(Integer) }.sum
159
+ rescue ::RedisClient::Cluster::ErrorCollection => e
160
+ raise if retry_count <= 0
161
+ raise if e.errors.values.none? do |err|
162
+ err.message.include?('WAIT cannot be used with replica instances')
163
+ end
164
+
165
+ update_cluster_info!
166
+ retry_count -= 1
167
+ retry
168
+ end
169
+
170
+ def send_config_command(method, *args, **kwargs, &block)
171
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
172
+ command = @command_builder.generate!(command, kwargs)
173
+
174
+ case command[1].to_s.downcase
175
+ when 'resetstat', 'rewrite', 'set'
176
+ @node.call_all(method, *args, **kwargs, &block).first
177
+ else assign_node(*command).send(method, *args, **kwargs, &block)
178
+ end
179
+ end
180
+
181
+ def send_memory_command(method, *args, **kwargs, &block)
182
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
183
+ command = @command_builder.generate!(command, kwargs)
184
+
185
+ case command[1].to_s.downcase
186
+ when 'stats' then @node.call_all(method, *args, **kwargs, &block)
187
+ when 'purge' then @node.call_all(method, *args, **kwargs, &block).first
188
+ else assign_node(*command).send(method, *args, **kwargs, &block)
189
+ end
190
+ end
191
+
192
+ def send_client_command(method, *args, **kwargs, &block)
193
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
194
+ command = @command_builder.generate!(command, kwargs)
195
+
196
+ case command[1].to_s.downcase
197
+ when 'list' then @node.call_all(method, *args, **kwargs, &block).flatten
198
+ when 'pause', 'reply', 'setname'
199
+ @node.call_all(method, *args, **kwargs, &block).first
200
+ else assign_node(*command).send(method, *args, **kwargs, &block)
201
+ end
202
+ end
203
+
204
+ def send_cluster_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
205
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
206
+ command = @command_builder.generate!(command, kwargs)
207
+ subcommand = command[1].to_s.downcase
208
+
209
+ case subcommand
210
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
211
+ 'reset', 'set-config-epoch', 'setslot'
212
+ raise ::RedisClient::Cluster::OrchestrationCommandNotSupported, ['cluster', subcommand]
213
+ when 'saveconfig' then @node.call_all(method, *args, **kwargs, &block).first
214
+ when 'getkeysinslot'
215
+ raise ArgumentError, command.join(' ') if command.size != 4
216
+
217
+ find_node(@node.find_node_key_of_replica(command[2])).send(method, *args, **kwargs, &block)
218
+ else assign_node(*command).send(method, *args, **kwargs, &block)
219
+ end
220
+ end
221
+
222
+ def send_script_command(method, *args, **kwargs, &block)
223
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
224
+ command = @command_builder.generate!(command, kwargs)
225
+
226
+ case command[1].to_s.downcase
227
+ when 'debug', 'kill'
228
+ @node.call_all(method, *args, **kwargs, &block).first
229
+ when 'flush', 'load'
230
+ @node.call_primaries(method, *args, **kwargs, &block).first
231
+ else assign_node(*command).send(method, *args, **kwargs, &block)
232
+ end
233
+ end
234
+
235
+ def send_pubsub_command(method, *args, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
236
+ command = method == :blocking_call && args.size > 1 ? args[1..] : args
237
+ command = @command_builder.generate!(command, kwargs)
238
+
239
+ case command[1].to_s.downcase
240
+ when 'channels' then @node.call_all(method, *args, **kwargs, &block).flatten.uniq.sort_by(&:to_s)
241
+ when 'numsub'
242
+ @node.call_all(method, *args, **kwargs, &block).reject(&:empty?).map { |e| Hash[*e] }
243
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
244
+ when 'numpat' then @node.call_all(method, *args, **kwargs, &block).select { |e| e.is_a?(Integer) }.sum
245
+ else assign_node(*command).send(method, *args, **kwargs, &block)
246
+ end
247
+ end
248
+
249
+ def assign_redirection_node(err_msg)
250
+ _, slot, node_key = err_msg.split
251
+ slot = slot.to_i
252
+ @node.update_slot(slot, node_key)
253
+ find_node(node_key)
254
+ end
255
+
256
+ def assign_asking_node(err_msg)
257
+ _, _, node_key = err_msg.split
258
+ find_node(node_key)
259
+ end
260
+
261
+ def fetch_cluster_info(config, pool: nil, **kwargs)
262
+ node_info = ::RedisClient::Cluster::Node.load_info(config.per_node_key, **kwargs)
263
+ node_addrs = node_info.map { |info| ::RedisClient::Cluster::NodeKey.hashify(info[:node_key]) }
264
+ config.update_node(node_addrs)
265
+ ::RedisClient::Cluster::Node.new(config.per_node_key,
266
+ node_info: node_info, pool: pool, with_replica: config.use_replica?, **kwargs)
267
+ end
268
+
269
+ def update_cluster_info!
270
+ @mutex.synchronize do
271
+ begin
272
+ @node.call_all(:close)
273
+ rescue ::RedisClient::Cluster::ErrorCollection
274
+ # ignore
275
+ end
276
+
277
+ @node = fetch_cluster_info(@config, pool: @pool, **@client_kwargs)
278
+ end
279
+ end
280
+ end
281
+ end
282
+ 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
- def call(*command, **kwargs)
122
- send_command(:call, *command, **kwargs)
20
+ def call(*command, **kwargs, &block)
21
+ @router.send_command(:call, *command, **kwargs, &block)
123
22
  end
124
23
 
125
- def call_once(*command, **kwargs)
126
- send_command(:call_once, *command, **kwargs)
24
+ def call_once(*command, **kwargs, &block)
25
+ @router.send_command(:call_once, *command, **kwargs, &block)
127
26
  end
128
27
 
129
- def blocking_call(timeout, *command, **kwargs)
130
- send_command(:blocking_call, timeout, *command, **kwargs)
28
+ def blocking_call(timeout, *command, **kwargs, &block)
29
+ @router.send_command(:blocking_call, timeout, *command, **kwargs, &block)
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,29 @@ 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
73
  end
177
74
 
178
75
  private
179
76
 
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')
77
+ def method_missing(name, *args, **kwargs, &block)
78
+ if @router.command_exists?(name)
79
+ args.unshift(name)
80
+ return @router.send_command(:call, *args, **kwargs, &block)
229
81
  end
230
82
 
231
- update_cluster_info!
232
- retry_count -= 1
233
- retry
83
+ super
234
84
  end
235
85
 
236
- def send_config_command(method, *args, **kwargs, &block)
237
- command = method == :blocking_call && args.size > 1 ? args[1..] : args
86
+ def respond_to_missing?(name, include_private = false)
87
+ return true if @router.command_exists?(name)
238
88
 
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
89
+ super
401
90
  end
402
91
  end
403
92
  end
@@ -4,6 +4,7 @@ require 'uri'
4
4
  require 'redis_client'
5
5
  require 'redis_client/cluster'
6
6
  require 'redis_client/cluster/node_key'
7
+ require 'redis_client/command_builder'
7
8
 
8
9
  class RedisClient
9
10
  class ClusterConfig
@@ -19,11 +20,14 @@ class RedisClient
19
20
 
20
21
  InvalidClientConfigError = Class.new(::RedisClient::Error)
21
22
 
23
+ attr_reader :command_builder
24
+
22
25
  def initialize(nodes: DEFAULT_NODES, replica: false, fixed_hostname: '', **client_config)
23
26
  @replica = true & replica
24
27
  @fixed_hostname = fixed_hostname.to_s
25
28
  @node_configs = build_node_configs(nodes.dup)
26
29
  client_config = client_config.reject { |k, _| IGNORE_GENERIC_CONFIG_KEYS.include?(k) }
30
+ @command_builder = client_config.fetch(:command_builder, ::RedisClient::CommandBuilder)
27
31
  @client_config = merge_generic_config(client_config, @node_configs)
28
32
  @mutex = Mutex.new
29
33
  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.1.0
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