redis 3.3.5 → 4.1.4

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +84 -2
  3. data/README.md +131 -76
  4. data/lib/redis.rb +912 -200
  5. data/lib/redis/client.rb +71 -29
  6. data/lib/redis/cluster.rb +291 -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 +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +87 -0
  14. data/lib/redis/cluster/slot.rb +72 -0
  15. data/lib/redis/cluster/slot_loader.rb +50 -0
  16. data/lib/redis/connection.rb +3 -2
  17. data/lib/redis/connection/command_helper.rb +3 -8
  18. data/lib/redis/connection/hiredis.rb +3 -2
  19. data/lib/redis/connection/registry.rb +1 -0
  20. data/lib/redis/connection/ruby.rb +48 -32
  21. data/lib/redis/connection/synchrony.rb +13 -4
  22. data/lib/redis/distributed.rb +39 -15
  23. data/lib/redis/errors.rb +47 -0
  24. data/lib/redis/hash_ring.rb +21 -64
  25. data/lib/redis/pipeline.rb +54 -12
  26. data/lib/redis/subscribe.rb +1 -0
  27. data/lib/redis/version.rb +2 -1
  28. metadata +40 -198
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -89
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb CHANGED
@@ -1,4 +1,6 @@
1
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
2
4
  require "socket"
3
5
  require "cgi"
4
6
 
@@ -18,12 +20,12 @@ class Redis
18
20
  :id => nil,
19
21
  :tcp_keepalive => 0,
20
22
  :reconnect_attempts => 1,
23
+ :reconnect_delay => 0,
24
+ :reconnect_delay_max => 0.5,
21
25
  :inherit_socket => false
22
26
  }
23
27
 
24
- def options
25
- Marshal.load(Marshal.dump(@options))
26
- end
28
+ attr_reader :options
27
29
 
28
30
  def scheme
29
31
  @options[:scheme]
@@ -86,11 +88,14 @@ class Redis
86
88
 
87
89
  @pending_reads = 0
88
90
 
89
- if options.include?(:sentinels)
90
- @connector = Connector::Sentinel.new(@options)
91
- else
92
- @connector = Connector.new(@options)
93
- end
91
+ @connector =
92
+ if options.include?(:sentinels)
93
+ Connector::Sentinel.new(@options)
94
+ elsif options.include?(:connector) && options[:connector].respond_to?(:new)
95
+ options.delete(:connector).new(@options)
96
+ else
97
+ Connector.new(@options)
98
+ end
94
99
  end
95
100
 
96
101
  def connect
@@ -152,9 +157,11 @@ class Redis
152
157
  end
153
158
 
154
159
  def call_pipeline(pipeline)
160
+ return [] if pipeline.futures.empty?
161
+
155
162
  with_reconnect pipeline.with_reconnect? do
156
163
  begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
164
+ pipeline.finish(call_pipelined(pipeline)).tap do
158
165
  self.db = pipeline.db if pipeline.db
159
166
  end
160
167
  rescue ConnectionError => e
@@ -167,8 +174,8 @@ class Redis
167
174
  end
168
175
  end
169
176
 
170
- def call_pipelined(commands)
171
- return [] if commands.empty?
177
+ def call_pipelined(pipeline)
178
+ return [] if pipeline.futures.empty?
172
179
 
173
180
  # The method #ensure_connected (called from #process) reconnects once on
174
181
  # I/O errors. To make an effort in making sure that commands are not
@@ -178,6 +185,8 @@ class Redis
178
185
  # already successfully executed commands. To circumvent this, don't retry
179
186
  # after the first reply has been read successfully.
180
187
 
188
+ commands = pipeline.commands
189
+
181
190
  result = Array.new(commands.size)
182
191
  reconnect = @reconnect
183
192
 
@@ -185,13 +194,14 @@ class Redis
185
194
  exception = nil
186
195
 
187
196
  process(commands) do
188
- result[0] = read
189
-
190
- @reconnect = false
191
-
192
- (commands.size - 1).times do |i|
193
- reply = read
194
- result[i + 1] = reply
197
+ pipeline.timeouts.each_with_index do |timeout, i|
198
+ reply = if timeout
199
+ with_socket_timeout(timeout) { read }
200
+ else
201
+ read
202
+ end
203
+ result[i] = reply
204
+ @reconnect = false
195
205
  exception = reply if exception.nil? && reply.is_a?(CommandError)
196
206
  end
197
207
  end
@@ -240,6 +250,7 @@ class Redis
240
250
  def disconnect
241
251
  connection.disconnect if connected?
242
252
  end
253
+ alias_method :close, :disconnect
243
254
 
244
255
  def reconnect
245
256
  disconnect
@@ -274,12 +285,15 @@ class Redis
274
285
 
275
286
  def with_socket_timeout(timeout)
276
287
  connect unless connected?
288
+ original = @options[:read_timeout]
277
289
 
278
290
  begin
279
291
  connection.timeout = timeout
292
+ @options[:read_timeout] = timeout # for reconnection
280
293
  yield
281
294
  ensure
282
295
  connection.timeout = self.timeout if connected?
296
+ @options[:read_timeout] = original
283
297
  end
284
298
  end
285
299
 
@@ -336,11 +350,15 @@ class Redis
336
350
  @connection = @options[:driver].connect(@options)
337
351
  @pending_reads = 0
338
352
  rescue TimeoutError,
353
+ SocketError,
354
+ Errno::EADDRNOTAVAIL,
339
355
  Errno::ECONNREFUSED,
340
356
  Errno::EHOSTDOWN,
341
357
  Errno::EHOSTUNREACH,
342
358
  Errno::ENETUNREACH,
343
- Errno::ETIMEDOUT
359
+ Errno::ENOENT,
360
+ Errno::ETIMEDOUT,
361
+ Errno::EINVAL
344
362
 
345
363
  raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
346
364
  end
@@ -369,6 +387,10 @@ class Redis
369
387
  disconnect
370
388
 
371
389
  if attempts <= @options[:reconnect_attempts] && @reconnect
390
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
391
+ @options[:reconnect_delay_max]].min
392
+
393
+ Kernel.sleep(sleep_t)
372
394
  retry
373
395
  else
374
396
  raise
@@ -395,7 +417,8 @@ class Redis
395
417
  options[key] = options[key.to_s] if options.has_key?(key.to_s)
396
418
  end
397
419
 
398
- url = options[:url] || defaults[:url]
420
+ url = options[:url]
421
+ url = defaults[:url] if url == nil
399
422
 
400
423
  # Override defaults from URL if given
401
424
  if url
@@ -445,6 +468,10 @@ class Redis
445
468
  options[:read_timeout] = Float(options[:read_timeout])
446
469
  options[:write_timeout] = Float(options[:write_timeout])
447
470
 
471
+ options[:reconnect_attempts] = options[:reconnect_attempts].to_i
472
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
473
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
474
+
448
475
  options[:db] = options[:db].to_i
449
476
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
477
 
@@ -478,11 +505,16 @@ class Redis
478
505
 
479
506
  if driver.kind_of?(String)
480
507
  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}"
508
+ require_relative "connection/#{driver}"
509
+ rescue LoadError, NameError => e
510
+ begin
511
+ require "connection/#{driver}"
512
+ rescue LoadError, NameError => e
513
+ raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
514
+ end
485
515
  end
516
+
517
+ driver = Connection.const_get(driver.capitalize)
486
518
  end
487
519
 
488
520
  driver
@@ -504,7 +536,6 @@ class Redis
504
536
  def initialize(options)
505
537
  super(options)
506
538
 
507
- @options[:password] = DEFAULTS.fetch(:password)
508
539
  @options[:db] = DEFAULTS.fetch(:db)
509
540
 
510
541
  @sentinels = @options.delete(:sentinels).dup
@@ -547,6 +578,7 @@ class Redis
547
578
  client = Client.new(@options.merge({
548
579
  :host => sentinel[:host],
549
580
  :port => sentinel[:port],
581
+ password: sentinel[:password],
550
582
  :reconnect_attempts => 0,
551
583
  }))
552
584
 
@@ -578,9 +610,19 @@ class Redis
578
610
  def resolve_slave
579
611
  sentinel_detect do |client|
580
612
  if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
613
+ slaves = reply.map { |s| s.each_slice(2).to_h }
614
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
615
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
616
+
617
+ if slaves.empty?
618
+ raise CannotConnectError, 'No slaves available.'
619
+ else
620
+ slave = slaves.sample
621
+ {
622
+ host: slave.fetch('ip'),
623
+ port: slave.fetch('port'),
624
+ }
625
+ end
584
626
  end
585
627
  end
586
628
  end
@@ -0,0 +1,291 @@
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
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
116
+ [Node.new(option.per_node_key, node_flags, option.use_replica?),
117
+ Slot.new(available_slots, node_flags, option.use_replica?)]
118
+ ensure
119
+ node&.each(&:disconnect)
120
+ end
121
+
122
+ def fetch_command_details(nodes)
123
+ details = CommandLoader.load(nodes)
124
+ Command.new(details)
125
+ end
126
+
127
+ def send_command(command, &block)
128
+ cmd = command.first.to_s.downcase
129
+ case cmd
130
+ when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
131
+ @node.call_all(command, &block).first
132
+ when 'flushall', 'flushdb'
133
+ @node.call_master(command, &block).first
134
+ when 'wait' then @node.call_master(command, &block).reduce(:+)
135
+ when 'keys' then @node.call_slave(command, &block).flatten.sort
136
+ when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
137
+ when 'lastsave' then @node.call_all(command, &block).sort
138
+ when 'role' then @node.call_all(command, &block)
139
+ when 'config' then send_config_command(command, &block)
140
+ when 'client' then send_client_command(command, &block)
141
+ when 'cluster' then send_cluster_command(command, &block)
142
+ when 'readonly', 'readwrite', 'shutdown'
143
+ raise OrchestrationCommandNotSupported, cmd
144
+ when 'memory' then send_memory_command(command, &block)
145
+ when 'script' then send_script_command(command, &block)
146
+ when 'pubsub' then send_pubsub_command(command, &block)
147
+ when 'discard', 'exec', 'multi', 'unwatch'
148
+ raise AmbiguousNodeError, cmd
149
+ else
150
+ node = assign_node(command)
151
+ try_send(node, :call, command, &block)
152
+ end
153
+ end
154
+
155
+ def send_config_command(command, &block)
156
+ case command[1].to_s.downcase
157
+ when 'resetstat', 'rewrite', 'set'
158
+ @node.call_all(command, &block).first
159
+ else assign_node(command).call(command, &block)
160
+ end
161
+ end
162
+
163
+ def send_memory_command(command, &block)
164
+ case command[1].to_s.downcase
165
+ when 'stats' then @node.call_all(command, &block)
166
+ when 'purge' then @node.call_all(command, &block).first
167
+ else assign_node(command).call(command, &block)
168
+ end
169
+ end
170
+
171
+ def send_client_command(command, &block)
172
+ case command[1].to_s.downcase
173
+ when 'list' then @node.call_all(command, &block).flatten
174
+ when 'pause', 'reply', 'setname'
175
+ @node.call_all(command, &block).first
176
+ else assign_node(command).call(command, &block)
177
+ end
178
+ end
179
+
180
+ def send_cluster_command(command, &block)
181
+ subcommand = command[1].to_s.downcase
182
+ case subcommand
183
+ when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
184
+ 'reset', 'set-config-epoch', 'setslot'
185
+ raise OrchestrationCommandNotSupported, 'cluster', subcommand
186
+ when 'saveconfig' then @node.call_all(command, &block).first
187
+ else assign_node(command).call(command, &block)
188
+ end
189
+ end
190
+
191
+ def send_script_command(command, &block)
192
+ case command[1].to_s.downcase
193
+ when 'debug', 'kill'
194
+ @node.call_all(command, &block).first
195
+ when 'flush', 'load'
196
+ @node.call_master(command, &block).first
197
+ else assign_node(command).call(command, &block)
198
+ end
199
+ end
200
+
201
+ def send_pubsub_command(command, &block)
202
+ case command[1].to_s.downcase
203
+ when 'channels' then @node.call_all(command, &block).flatten.uniq.sort
204
+ when 'numsub'
205
+ @node.call_all(command, &block).reject(&:empty?).map { |e| Hash[*e] }
206
+ .reduce({}) { |a, e| a.merge(e) { |_, v1, v2| v1 + v2 } }
207
+ when 'numpat' then @node.call_all(command, &block).reduce(:+)
208
+ else assign_node(command).call(command, &block)
209
+ end
210
+ end
211
+
212
+ # @see https://redis.io/topics/cluster-spec#redirection-and-resharding
213
+ # Redirection and resharding
214
+ def try_send(node, method_name, *args, retry_count: 3, &block)
215
+ node.public_send(method_name, *args, &block)
216
+ rescue CommandError => err
217
+ if err.message.start_with?('MOVED')
218
+ raise if retry_count <= 0
219
+ node = assign_redirection_node(err.message)
220
+ retry_count -= 1
221
+ retry
222
+ elsif err.message.start_with?('ASK')
223
+ raise if retry_count <= 0
224
+ node = assign_asking_node(err.message)
225
+ node.call(%i[asking])
226
+ retry_count -= 1
227
+ retry
228
+ else
229
+ raise
230
+ end
231
+ rescue CannotConnectError
232
+ update_cluster_info!
233
+ raise
234
+ end
235
+
236
+ def assign_redirection_node(err_msg)
237
+ _, slot, node_key = err_msg.split(' ')
238
+ slot = slot.to_i
239
+ @slot.put(slot, node_key)
240
+ find_node(node_key)
241
+ end
242
+
243
+ def assign_asking_node(err_msg)
244
+ _, _, node_key = err_msg.split(' ')
245
+ find_node(node_key)
246
+ end
247
+
248
+ def assign_node(command)
249
+ node_key = find_node_key(command)
250
+ find_node(node_key)
251
+ end
252
+
253
+ def find_node_key(command)
254
+ key = @command.extract_first_key(command)
255
+ return if key.empty?
256
+
257
+ slot = KeySlotConverter.convert(key)
258
+ return unless @slot.exists?(slot)
259
+
260
+ if @command.should_send_to_master?(command)
261
+ @slot.find_node_key_of_master(slot)
262
+ else
263
+ @slot.find_node_key_of_slave(slot)
264
+ end
265
+ end
266
+
267
+ def find_node(node_key)
268
+ return @node.sample if node_key.nil?
269
+ @node.find_by(node_key)
270
+ rescue Node::ReloadNeeded
271
+ update_cluster_info!(node_key)
272
+ @node.find_by(node_key)
273
+ end
274
+
275
+ def update_cluster_info!(node_key = nil)
276
+ unless node_key.nil?
277
+ host, port = NodeKey.split(node_key)
278
+ @option.add_node(host, port)
279
+ end
280
+
281
+ @node.map(&:disconnect)
282
+ @node, @slot = fetch_cluster_info!(@option)
283
+ end
284
+
285
+ def extract_keys_in_pipeline(pipeline)
286
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
287
+ command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
288
+ [node_keys, command_keys]
289
+ end
290
+ end
291
+ end