redis 3.3.5 → 4.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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -2
  3. data/README.md +77 -76
  4. data/lib/redis.rb +779 -63
  5. data/lib/redis/client.rb +41 -20
  6. data/lib/redis/cluster.rb +286 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +104 -0
  11. data/lib/redis/cluster/node_key.rb +35 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +77 -0
  14. data/lib/redis/cluster/slot.rb +69 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +2 -2
  17. data/lib/redis/connection/command_helper.rb +2 -8
  18. data/lib/redis/connection/hiredis.rb +2 -2
  19. data/lib/redis/connection/ruby.rb +13 -30
  20. data/lib/redis/connection/synchrony.rb +12 -4
  21. data/lib/redis/distributed.rb +32 -12
  22. data/lib/redis/errors.rb +46 -0
  23. data/lib/redis/hash_ring.rb +20 -64
  24. data/lib/redis/pipeline.rb +9 -7
  25. data/lib/redis/version.rb +1 -1
  26. metadata +53 -196
  27. data/.gitignore +0 -16
  28. data/.travis.yml +0 -89
  29. data/.travis/Gemfile +0 -11
  30. data/.yardopts +0 -3
  31. data/Gemfile +0 -4
  32. data/Rakefile +0 -87
  33. data/benchmarking/logging.rb +0 -71
  34. data/benchmarking/pipeline.rb +0 -51
  35. data/benchmarking/speed.rb +0 -21
  36. data/benchmarking/suite.rb +0 -24
  37. data/benchmarking/worker.rb +0 -71
  38. data/examples/basic.rb +0 -15
  39. data/examples/consistency.rb +0 -114
  40. data/examples/dist_redis.rb +0 -43
  41. data/examples/incr-decr.rb +0 -17
  42. data/examples/list.rb +0 -26
  43. data/examples/pubsub.rb +0 -37
  44. data/examples/sentinel.rb +0 -41
  45. data/examples/sentinel/start +0 -49
  46. data/examples/sets.rb +0 -36
  47. data/examples/unicorn/config.ru +0 -3
  48. data/examples/unicorn/unicorn.rb +0 -20
  49. data/redis.gemspec +0 -44
  50. data/test/bitpos_test.rb +0 -69
  51. data/test/blocking_commands_test.rb +0 -42
  52. data/test/client_test.rb +0 -59
  53. data/test/command_map_test.rb +0 -30
  54. data/test/commands_on_hashes_test.rb +0 -21
  55. data/test/commands_on_hyper_log_log_test.rb +0 -21
  56. data/test/commands_on_lists_test.rb +0 -20
  57. data/test/commands_on_sets_test.rb +0 -77
  58. data/test/commands_on_sorted_sets_test.rb +0 -137
  59. data/test/commands_on_strings_test.rb +0 -101
  60. data/test/commands_on_value_types_test.rb +0 -133
  61. data/test/connection_handling_test.rb +0 -277
  62. data/test/connection_test.rb +0 -57
  63. data/test/distributed_blocking_commands_test.rb +0 -46
  64. data/test/distributed_commands_on_hashes_test.rb +0 -10
  65. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  66. data/test/distributed_commands_on_lists_test.rb +0 -22
  67. data/test/distributed_commands_on_sets_test.rb +0 -83
  68. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  69. data/test/distributed_commands_on_strings_test.rb +0 -59
  70. data/test/distributed_commands_on_value_types_test.rb +0 -95
  71. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  72. data/test/distributed_connection_handling_test.rb +0 -23
  73. data/test/distributed_internals_test.rb +0 -79
  74. data/test/distributed_key_tags_test.rb +0 -52
  75. data/test/distributed_persistence_control_commands_test.rb +0 -26
  76. data/test/distributed_publish_subscribe_test.rb +0 -92
  77. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  78. data/test/distributed_scripting_test.rb +0 -102
  79. data/test/distributed_sorting_test.rb +0 -20
  80. data/test/distributed_test.rb +0 -58
  81. data/test/distributed_transactions_test.rb +0 -32
  82. data/test/encoding_test.rb +0 -18
  83. data/test/error_replies_test.rb +0 -59
  84. data/test/fork_safety_test.rb +0 -65
  85. data/test/helper.rb +0 -232
  86. data/test/helper_test.rb +0 -24
  87. data/test/internals_test.rb +0 -417
  88. data/test/lint/blocking_commands.rb +0 -150
  89. data/test/lint/hashes.rb +0 -162
  90. data/test/lint/hyper_log_log.rb +0 -60
  91. data/test/lint/lists.rb +0 -143
  92. data/test/lint/sets.rb +0 -140
  93. data/test/lint/sorted_sets.rb +0 -316
  94. data/test/lint/strings.rb +0 -260
  95. data/test/lint/value_types.rb +0 -122
  96. data/test/persistence_control_commands_test.rb +0 -26
  97. data/test/pipelining_commands_test.rb +0 -242
  98. data/test/publish_subscribe_test.rb +0 -282
  99. data/test/remote_server_control_commands_test.rb +0 -118
  100. data/test/scanning_test.rb +0 -413
  101. data/test/scripting_test.rb +0 -78
  102. data/test/sentinel_command_test.rb +0 -80
  103. data/test/sentinel_test.rb +0 -255
  104. data/test/sorting_test.rb +0 -59
  105. data/test/ssl_test.rb +0 -73
  106. data/test/support/connection/hiredis.rb +0 -1
  107. data/test/support/connection/ruby.rb +0 -1
  108. data/test/support/connection/synchrony.rb +0 -17
  109. data/test/support/redis_mock.rb +0 -130
  110. data/test/support/ssl/gen_certs.sh +0 -31
  111. data/test/support/ssl/trusted-ca.crt +0 -25
  112. data/test/support/ssl/trusted-ca.key +0 -27
  113. data/test/support/ssl/trusted-cert.crt +0 -81
  114. data/test/support/ssl/trusted-cert.key +0 -28
  115. data/test/support/ssl/untrusted-ca.crt +0 -26
  116. data/test/support/ssl/untrusted-ca.key +0 -27
  117. data/test/support/ssl/untrusted-cert.crt +0 -82
  118. data/test/support/ssl/untrusted-cert.key +0 -28
  119. data/test/support/wire/synchrony.rb +0 -24
  120. data/test/support/wire/thread.rb +0 -5
  121. data/test/synchrony_driver.rb +0 -88
  122. data/test/test.conf.erb +0 -9
  123. data/test/thread_safety_test.rb +0 -62
  124. data/test/transactions_test.rb +0 -264
  125. data/test/unknown_commands_test.rb +0 -14
  126. data/test/url_param_test.rb +0 -138
@@ -1,4 +1,4 @@
1
- require "redis/errors"
1
+ require_relative "errors"
2
2
  require "socket"
3
3
  require "cgi"
4
4
 
@@ -18,12 +18,12 @@ class Redis
18
18
  :id => nil,
19
19
  :tcp_keepalive => 0,
20
20
  :reconnect_attempts => 1,
21
+ :reconnect_delay => 0,
22
+ :reconnect_delay_max => 0.5,
21
23
  :inherit_socket => false
22
24
  }
23
25
 
24
- def options
25
- Marshal.load(Marshal.dump(@options))
26
- end
26
+ attr_reader :options
27
27
 
28
28
  def scheme
29
29
  @options[:scheme]
@@ -86,11 +86,14 @@ class Redis
86
86
 
87
87
  @pending_reads = 0
88
88
 
89
- if options.include?(:sentinels)
90
- @connector = Connector::Sentinel.new(@options)
91
- else
92
- @connector = Connector.new(@options)
93
- end
89
+ @connector =
90
+ if options.include?(:sentinels)
91
+ Connector::Sentinel.new(@options)
92
+ elsif options.include?(:connector) && options[:connector].respond_to?(:new)
93
+ options.delete(:connector).new(@options)
94
+ else
95
+ Connector.new(@options)
96
+ end
94
97
  end
95
98
 
96
99
  def connect
@@ -152,9 +155,12 @@ class Redis
152
155
  end
153
156
 
154
157
  def call_pipeline(pipeline)
158
+ commands = pipeline.commands
159
+ return [] if commands.empty?
160
+
155
161
  with_reconnect pipeline.with_reconnect? do
156
162
  begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
163
+ pipeline.finish(call_pipelined(commands)).tap do
158
164
  self.db = pipeline.db if pipeline.db
159
165
  end
160
166
  rescue ConnectionError => e
@@ -185,13 +191,10 @@ class Redis
185
191
  exception = nil
186
192
 
187
193
  process(commands) do
188
- result[0] = read
189
-
190
- @reconnect = false
191
-
192
- (commands.size - 1).times do |i|
194
+ commands.size.times do |i|
193
195
  reply = read
194
- result[i + 1] = reply
196
+ result[i] = reply
197
+ @reconnect = false
195
198
  exception = reply if exception.nil? && reply.is_a?(CommandError)
196
199
  end
197
200
  end
@@ -274,12 +277,15 @@ class Redis
274
277
 
275
278
  def with_socket_timeout(timeout)
276
279
  connect unless connected?
280
+ original = @options[:read_timeout]
277
281
 
278
282
  begin
279
283
  connection.timeout = timeout
284
+ @options[:read_timeout] = timeout # for reconnection
280
285
  yield
281
286
  ensure
282
287
  connection.timeout = self.timeout if connected?
288
+ @options[:read_timeout] = original
283
289
  end
284
290
  end
285
291
 
@@ -336,10 +342,12 @@ class Redis
336
342
  @connection = @options[:driver].connect(@options)
337
343
  @pending_reads = 0
338
344
  rescue TimeoutError,
345
+ SocketError,
339
346
  Errno::ECONNREFUSED,
340
347
  Errno::EHOSTDOWN,
341
348
  Errno::EHOSTUNREACH,
342
349
  Errno::ENETUNREACH,
350
+ Errno::ENOENT,
343
351
  Errno::ETIMEDOUT
344
352
 
345
353
  raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
@@ -369,6 +377,10 @@ class Redis
369
377
  disconnect
370
378
 
371
379
  if attempts <= @options[:reconnect_attempts] && @reconnect
380
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
381
+ @options[:reconnect_delay_max]].min
382
+
383
+ Kernel.sleep(sleep_t)
372
384
  retry
373
385
  else
374
386
  raise
@@ -445,6 +457,10 @@ class Redis
445
457
  options[:read_timeout] = Float(options[:read_timeout])
446
458
  options[:write_timeout] = Float(options[:write_timeout])
447
459
 
460
+ options[:reconnect_attempts] = options[:reconnect_attempts].to_i
461
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
462
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
463
+
448
464
  options[:db] = options[:db].to_i
449
465
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
466
 
@@ -478,11 +494,16 @@ class Redis
478
494
 
479
495
  if driver.kind_of?(String)
480
496
  begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
483
- rescue LoadError, NameError
484
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
497
+ require_relative "connection/#{driver}"
498
+ rescue LoadError, NameError => e
499
+ begin
500
+ require "connection/#{driver}"
501
+ rescue LoadError, NameError => e
502
+ raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
503
+ end
485
504
  end
505
+
506
+ driver = Connection.const_get(driver.capitalize)
486
507
  end
487
508
 
488
509
  driver
@@ -0,0 +1,286 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+ require_relative 'client'
5
+ require_relative 'cluster/command'
6
+ require_relative 'cluster/command_loader'
7
+ require_relative 'cluster/key_slot_converter'
8
+ require_relative 'cluster/node'
9
+ require_relative 'cluster/node_key'
10
+ require_relative 'cluster/node_loader'
11
+ require_relative 'cluster/option'
12
+ require_relative 'cluster/slot'
13
+ require_relative 'cluster/slot_loader'
14
+
15
+ class Redis
16
+ # Redis Cluster client
17
+ #
18
+ # @see https://github.com/antirez/redis-rb-cluster POC implementation
19
+ # @see https://redis.io/topics/cluster-spec Redis Cluster specification
20
+ # @see https://redis.io/topics/cluster-tutorial Redis Cluster tutorial
21
+ #
22
+ # Copyright (C) 2013 Salvatore Sanfilippo <antirez@gmail.com>
23
+ class Cluster
24
+ def initialize(options = {})
25
+ @option = Option.new(options)
26
+ @node, @slot = fetch_cluster_info!(@option)
27
+ @command = fetch_command_details(@node)
28
+ end
29
+
30
+ def id
31
+ @node.map(&:id).sort.join(' ')
32
+ end
33
+
34
+ # db feature is disabled in cluster mode
35
+ def db
36
+ 0
37
+ end
38
+
39
+ # db feature is disabled in cluster mode
40
+ def db=(_db); end
41
+
42
+ def timeout
43
+ @node.first.timeout
44
+ end
45
+
46
+ def connected?
47
+ @node.any?(&:connected?)
48
+ end
49
+
50
+ def disconnect
51
+ @node.each(&:disconnect)
52
+ true
53
+ end
54
+
55
+ def connection_info
56
+ @node.sort_by(&:id).map do |client|
57
+ {
58
+ host: client.host,
59
+ port: client.port,
60
+ db: client.db,
61
+ id: client.id,
62
+ location: client.location
63
+ }
64
+ end
65
+ end
66
+
67
+ def with_reconnect(val = true, &block)
68
+ try_send(@node.sample, :with_reconnect, val, &block)
69
+ end
70
+
71
+ def call(command, &block)
72
+ send_command(command, &block)
73
+ end
74
+
75
+ def call_loop(command, timeout = 0, &block)
76
+ node = assign_node(command)
77
+ try_send(node, :call_loop, command, timeout, &block)
78
+ end
79
+
80
+ def call_pipeline(pipeline)
81
+ node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
+ raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
83
+ node = find_node(node_keys.first)
84
+ try_send(node, :call_pipeline, pipeline)
85
+ end
86
+
87
+ def call_with_timeout(command, timeout, &block)
88
+ node = assign_node(command)
89
+ try_send(node, :call_with_timeout, command, timeout, &block)
90
+ end
91
+
92
+ def call_without_timeout(command, &block)
93
+ call_with_timeout(command, 0, &block)
94
+ end
95
+
96
+ def process(commands, &block)
97
+ if commands.size == 1 &&
98
+ %w[unsubscribe punsubscribe].include?(commands.first.first.to_s.downcase) &&
99
+ commands.first.size == 1
100
+
101
+ # Node is indeterminate. We do just a best-effort try here.
102
+ @node.process_all(commands, &block)
103
+ else
104
+ node = assign_node(commands.first)
105
+ try_send(node, :process, commands, &block)
106
+ end
107
+ end
108
+
109
+ private
110
+
111
+ def fetch_cluster_info!(option)
112
+ node = Node.new(option.per_node_key)
113
+ available_slots = SlotLoader.load(node)
114
+ node_flags = NodeLoader.load_flags(node)
115
+ available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
116
+ option.update_node(available_node_urls)
117
+ [Node.new(option.per_node_key, node_flags, option.use_replica?),
118
+ Slot.new(available_slots, node_flags, option.use_replica?)]
119
+ ensure
120
+ node.map(&:disconnect)
121
+ end
122
+
123
+ def fetch_command_details(nodes)
124
+ details = CommandLoader.load(nodes)
125
+ Command.new(details)
126
+ end
127
+
128
+ def send_command(command, &block)
129
+ cmd = command.first.to_s.downcase
130
+ case cmd
131
+ when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
+ @node.call_all(command, &block).first
133
+ when 'flushall', 'flushdb'
134
+ @node.call_master(command, &block).first
135
+ when 'wait' then @node.call_master(command, &block).reduce(:+)
136
+ when 'keys' then @node.call_slave(command, &block).flatten.sort
137
+ when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
138
+ when 'lastsave' then @node.call_all(command, &block).sort
139
+ when 'role' then @node.call_all(command, &block)
140
+ when 'config' then send_config_command(command, &block)
141
+ when 'client' then send_client_command(command, &block)
142
+ when 'cluster' then send_cluster_command(command, &block)
143
+ when 'readonly', 'readwrite', 'shutdown'
144
+ raise OrchestrationCommandNotSupported, cmd
145
+ when 'memory' then send_memory_command(command, &block)
146
+ when 'script' then send_script_command(command, &block)
147
+ when 'pubsub' then send_pubsub_command(command, &block)
148
+ when 'discard', 'exec', 'multi', 'unwatch'
149
+ raise AmbiguousNodeError, cmd
150
+ else
151
+ node = assign_node(command)
152
+ try_send(node, :call, command, &block)
153
+ end
154
+ end
155
+
156
+ def send_config_command(command, &block)
157
+ case command[1].to_s.downcase
158
+ when 'resetstat', 'rewrite', 'set'
159
+ @node.call_all(command, &block).first
160
+ else assign_node(command).call(command, &block)
161
+ end
162
+ end
163
+
164
+ def send_memory_command(command, &block)
165
+ case command[1].to_s.downcase
166
+ when 'stats' then @node.call_all(command, &block)
167
+ when 'purge' then @node.call_all(command, &block).first
168
+ else assign_node(command).call(command, &block)
169
+ end
170
+ end
171
+
172
+ def send_client_command(command, &block)
173
+ case command[1].to_s.downcase
174
+ when 'list' then @node.call_all(command, &block).flatten
175
+ when 'pause', 'reply', 'setname'
176
+ @node.call_all(command, &block).first
177
+ else assign_node(command).call(command, &block)
178
+ end
179
+ end
180
+
181
+ def send_cluster_command(command, &block)
182
+ subcommand = command[1].to_s.downcase
183
+ case subcommand
184
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
185
+ 'reset', 'set-config-epoch', 'setslot'
186
+ raise OrchestrationCommandNotSupported, 'cluster', subcommand
187
+ when 'saveconfig' then @node.call_all(command, &block).first
188
+ else assign_node(command).call(command, &block)
189
+ end
190
+ end
191
+
192
+ def send_script_command(command, &block)
193
+ case command[1].to_s.downcase
194
+ when 'debug', 'kill'
195
+ @node.call_all(command, &block).first
196
+ when 'flush', 'load'
197
+ @node.call_master(command, &block).first
198
+ else assign_node(command).call(command, &block)
199
+ end
200
+ end
201
+
202
+ def send_pubsub_command(command, &block)
203
+ case command[1].to_s.downcase
204
+ when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
205
+ when 'numsub'
206
+ @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
207
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
208
+ when 'numpat' then @node.call_all(command, &block).reduce(:+)
209
+ else assign_node(command).call(command, &block)
210
+ end
211
+ end
212
+
213
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
214
+ # Redirection and resharding
215
+ def try_send(node, method_name, *args, retry_count: 3, &block)
216
+ node.public_send(method_name, *args, &block)
217
+ rescue CommandError => err
218
+ if err.message.start_with?('MOVED')
219
+ assign_redirection_node(err.message).public_send(method_name, *args, &block)
220
+ elsif err.message.start_with?('ASK')
221
+ raise if retry_count <= 0
222
+ node = assign_asking_node(err.message)
223
+ node.call(%i[asking])
224
+ retry_count -= 1
225
+ retry
226
+ else
227
+ raise
228
+ end
229
+ end
230
+
231
+ def assign_redirection_node(err_msg)
232
+ _, slot, node_key = err_msg.split(' ')
233
+ slot = slot.to_i
234
+ @slot.put(slot, node_key)
235
+ find_node(node_key)
236
+ end
237
+
238
+ def assign_asking_node(err_msg)
239
+ _, _, node_key = err_msg.split(' ')
240
+ find_node(node_key)
241
+ end
242
+
243
+ def assign_node(command)
244
+ node_key = find_node_key(command)
245
+ find_node(node_key)
246
+ end
247
+
248
+ def find_node_key(command)
249
+ key = @command.extract_first_key(command)
250
+ return if key.empty?
251
+
252
+ slot = KeySlotConverter.convert(key)
253
+ return unless @slot.exists?(slot)
254
+
255
+ if @command.should_send_to_master?(command)
256
+ @slot.find_node_key_of_master(slot)
257
+ else
258
+ @slot.find_node_key_of_slave(slot)
259
+ end
260
+ end
261
+
262
+ def find_node(node_key)
263
+ return @node.sample if node_key.nil?
264
+ @node.find_by(node_key)
265
+ rescue Node::ReloadNeeded
266
+ update_cluster_info!(node_key)
267
+ @node.find_by(node_key)
268
+ end
269
+
270
+ def update_cluster_info!(node_key = nil)
271
+ unless node_key.nil?
272
+ host, port = NodeKey.split(node_key)
273
+ @option.add_node(host, port)
274
+ end
275
+
276
+ @node.map(&:disconnect)
277
+ @node, @slot = fetch_cluster_info!(@option)
278
+ end
279
+
280
+ def extract_keys_in_pipeline(pipeline)
281
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
282
+ command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
283
+ [node_keys, command_keys]
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep details about Redis commands for Redis Cluster Client.
8
+ # @see https://redis.io/commands/command
9
+ class Command
10
+ def initialize(details)
11
+ @details = pick_details(details)
12
+ end
13
+
14
+ def extract_first_key(command)
15
+ i = determine_first_key_position(command)
16
+ return '' if i == 0
17
+
18
+ key = command[i].to_s
19
+ hash_tag = extract_hash_tag(key)
20
+ hash_tag.empty? ? key : hash_tag
21
+ end
22
+
23
+ def should_send_to_master?(command)
24
+ dig_details(command, :write)
25
+ end
26
+
27
+ def should_send_to_slave?(command)
28
+ dig_details(command, :readonly)
29
+ end
30
+
31
+ private
32
+
33
+ def pick_details(details)
34
+ details.map do |command, detail|
35
+ [command, {
36
+ first_key_position: detail[:first],
37
+ write: detail[:flags].include?('write'),
38
+ readonly: detail[:flags].include?('readonly')
39
+ }]
40
+ end.to_h
41
+ end
42
+
43
+ def dig_details(command, key)
44
+ name = command.first.to_s
45
+ return unless @details.key?(name)
46
+
47
+ @details.fetch(name).fetch(key)
48
+ end
49
+
50
+ def determine_first_key_position(command)
51
+ case command.first.to_s.downcase
52
+ when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
53
+ when 'object' then 2
54
+ when 'memory'
55
+ command[1].to_s.casecmp('usage').zero? ? 2 : 0
56
+ when 'scan', 'sscan', 'hscan', 'zscan'
57
+ determine_optional_key_position(command, 'match')
58
+ when 'xread', 'xreadgroup'
59
+ determine_optional_key_position(command, 'streams')
60
+ else
61
+ dig_details(command, :first_key_position).to_i
62
+ end
63
+ end
64
+
65
+ def determine_optional_key_position(command, option_name)
66
+ idx = command.map(&:to_s).map(&:downcase).index(option_name)
67
+ idx.nil? ? 0 : idx + 1
68
+ end
69
+
70
+ # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
71
+ def extract_hash_tag(key)
72
+ s = key.index('{')
73
+ e = key.index('}', s.to_i + 1)
74
+
75
+ return '' if s.nil? || e.nil?
76
+
77
+ key[s + 1..e - 1]
78
+ end
79
+ end
80
+ end
81
+ end