redis 4.0.2 → 4.0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -0
  3. data/.travis.yml +2 -2
  4. data/CHANGELOG.md +6 -0
  5. data/lib/redis.rb +97 -11
  6. data/lib/redis/client.rb +19 -11
  7. data/lib/redis/cluster.rb +285 -0
  8. data/lib/redis/cluster/command.rb +81 -0
  9. data/lib/redis/cluster/command_loader.rb +32 -0
  10. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  11. data/lib/redis/cluster/node.rb +104 -0
  12. data/lib/redis/cluster/node_key.rb +35 -0
  13. data/lib/redis/cluster/node_loader.rb +35 -0
  14. data/lib/redis/cluster/option.rb +76 -0
  15. data/lib/redis/cluster/slot.rb +69 -0
  16. data/lib/redis/cluster/slot_loader.rb +47 -0
  17. data/lib/redis/errors.rb +46 -0
  18. data/lib/redis/version.rb +1 -1
  19. data/makefile +54 -16
  20. data/redis.gemspec +2 -1
  21. data/test/client_test.rb +17 -0
  22. data/test/cluster_abnormal_state_test.rb +38 -0
  23. data/test/cluster_blocking_commands_test.rb +15 -0
  24. data/test/cluster_client_internals_test.rb +77 -0
  25. data/test/cluster_client_key_hash_tags_test.rb +88 -0
  26. data/test/cluster_client_options_test.rb +147 -0
  27. data/test/cluster_client_pipelining_test.rb +59 -0
  28. data/test/cluster_client_replicas_test.rb +36 -0
  29. data/test/cluster_client_slots_test.rb +94 -0
  30. data/test/cluster_client_transactions_test.rb +71 -0
  31. data/test/cluster_commands_on_cluster_test.rb +165 -0
  32. data/test/cluster_commands_on_connection_test.rb +40 -0
  33. data/test/cluster_commands_on_geo_test.rb +74 -0
  34. data/test/cluster_commands_on_hashes_test.rb +11 -0
  35. data/test/cluster_commands_on_hyper_log_log_test.rb +17 -0
  36. data/test/cluster_commands_on_keys_test.rb +134 -0
  37. data/test/cluster_commands_on_lists_test.rb +15 -0
  38. data/test/cluster_commands_on_pub_sub_test.rb +101 -0
  39. data/test/cluster_commands_on_scripting_test.rb +56 -0
  40. data/test/cluster_commands_on_server_test.rb +221 -0
  41. data/test/cluster_commands_on_sets_test.rb +39 -0
  42. data/test/cluster_commands_on_sorted_sets_test.rb +35 -0
  43. data/test/cluster_commands_on_streams_test.rb +196 -0
  44. data/test/cluster_commands_on_strings_test.rb +15 -0
  45. data/test/cluster_commands_on_transactions_test.rb +41 -0
  46. data/test/cluster_commands_on_value_types_test.rb +14 -0
  47. data/test/commands_on_hashes_test.rb +2 -14
  48. data/test/commands_on_hyper_log_log_test.rb +2 -14
  49. data/test/commands_on_lists_test.rb +2 -13
  50. data/test/commands_on_sets_test.rb +2 -70
  51. data/test/commands_on_sorted_sets_test.rb +2 -145
  52. data/test/commands_on_strings_test.rb +2 -94
  53. data/test/distributed_blocking_commands_test.rb +8 -0
  54. data/test/distributed_commands_on_hashes_test.rb +16 -3
  55. data/test/distributed_commands_on_hyper_log_log_test.rb +8 -13
  56. data/test/distributed_commands_on_lists_test.rb +4 -5
  57. data/test/distributed_commands_on_sets_test.rb +45 -46
  58. data/test/distributed_commands_on_sorted_sets_test.rb +51 -8
  59. data/test/distributed_commands_on_strings_test.rb +10 -0
  60. data/test/helper.rb +176 -32
  61. data/test/internals_test.rb +13 -0
  62. data/test/lint/blocking_commands.rb +40 -16
  63. data/test/lint/hashes.rb +26 -0
  64. data/test/lint/hyper_log_log.rb +15 -1
  65. data/test/lint/lists.rb +16 -0
  66. data/test/lint/sets.rb +142 -0
  67. data/test/lint/sorted_sets.rb +183 -2
  68. data/test/lint/strings.rb +102 -0
  69. data/test/support/cluster/orchestrator.rb +199 -0
  70. metadata +79 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e8fe8cb80ceaefd35f21b3ae06bdfa7594a1878a
4
- data.tar.gz: ec9c330b13ac9639b6033c96b9e92da659eede4e
3
+ metadata.gz: 1ec178932a6874e8ac7a4f6cfa4390c8223367ec
4
+ data.tar.gz: 986e6c5d1729b9ec8f7d66900e5d9ad9437e29f9
5
5
  SHA512:
6
- metadata.gz: 215cf01a2c7987aafb818959f1dd70688099b48f51fbc55d0bacc2ebddc27d45f7a60aca57e412a80f94dc6d9316312fa29be1e1077092a92657bd75103cae2a
7
- data.tar.gz: f26724045477ec4d5d5351611266489c13f778fe2eed7ace4824f37dcadfc378a69271abe22166fc2da16da0ac0b5236448828d36a0c44146a22dfe0d886045e
6
+ metadata.gz: 14db8cb42f08014ebd942c39b5f119a68a8ec50fec046572bb3afc7708634589311b9bea0d4153bfd1d7cb1c46a94a00d77613872b2cb8c10c33f132755cd62d
7
+ data.tar.gz: efe0db683450fcb30b6d23b8c9448d6925b5c5d95b847f9563405a61cc7e667bbf8aea22642dcc9d8a1b716dc9c8cb55c562c86a08223e020991f69eff7df7f7
data/.gitignore CHANGED
@@ -5,6 +5,7 @@ Gemfile.lock
5
5
  /tmp/
6
6
  /.idea
7
7
  /.yardoc
8
+ /.bundle
8
9
  /coverage/*
9
10
  /doc/
10
11
  /examples/sentinel/sentinel.conf
@@ -14,3 +15,5 @@ Gemfile.lock
14
15
  /redis/*
15
16
  /test/db
16
17
  /test/test.conf
18
+ appendonly.aof
19
+ temp-rewriteaof-*.aof
@@ -6,7 +6,7 @@ before_install:
6
6
  - gem update --system 2.6.14
7
7
  - gem --version
8
8
 
9
- script: make test
9
+ script: make
10
10
 
11
11
  rvm:
12
12
  - 2.2.2
@@ -25,7 +25,7 @@ before_script:
25
25
  env:
26
26
  global:
27
27
  - VERBOSE=true
28
- - TIMEOUT=1
28
+ - TIMEOUT=9
29
29
  matrix:
30
30
  - DRIVER=ruby REDIS_BRANCH=3.0
31
31
  - DRIVER=ruby REDIS_BRANCH=3.2
@@ -1,3 +1,9 @@
1
+ # 4.0.3
2
+
3
+ * Fix raising command error for first command in pipeline. See #788.
4
+ * Fix the gemspec to stop exposing a `build` executable. See #785.
5
+ * Add `:reconnect_delay` and `:reconnect_delay_max` options. See #778.
6
+
1
7
  # 4.0.2
2
8
 
3
9
  * Added `Redis#unlink`. See #766.
@@ -31,11 +31,16 @@ class Redis
31
31
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
32
32
  # @option options [Array] :sentinels List of sentinels to contact
33
33
  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
34
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
35
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
36
+ # @option options [Class] :connector Class of custom connector
34
37
  #
35
38
  # @return [Redis] a new client instance
36
39
  def initialize(options = {})
37
40
  @options = options.dup
38
- @original_client = @client = Client.new(options)
41
+ @cluster_mode = options.key?(:cluster)
42
+ client = @cluster_mode ? Cluster : Client
43
+ @original_client = @client = client.new(options)
39
44
  @queue = Hash.new { |h, k| h[k] = [] }
40
45
 
41
46
  super() # Monitor#initialize
@@ -273,9 +278,7 @@ class Redis
273
278
  synchronize do |client|
274
279
  client.call([:info, cmd].compact) do |reply|
275
280
  if reply.kind_of?(String)
276
- reply = Hash[reply.split("\r\n").map do |line|
277
- line.split(":", 2) unless line =~ /^(#|$)/
278
- end.compact]
281
+ reply = HashifyInfo.call(reply)
279
282
 
280
283
  if cmd && cmd.to_s == "commandstats"
281
284
  # Extract nested hashes for INFO COMMANDSTATS
@@ -2827,6 +2830,41 @@ class Redis
2827
2830
  end
2828
2831
  end
2829
2832
 
2833
+ # Sends `CLUSTER *` command to random node and returns its reply.
2834
+ #
2835
+ # @see https://redis.io/commands#cluster Reference of cluster command
2836
+ #
2837
+ # @param subcommand [String, Symbol] the subcommand of cluster command
2838
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
2839
+ #
2840
+ # @return [Object] depends on the subcommand
2841
+ def cluster(subcommand, *args)
2842
+ subcommand = subcommand.to_s.downcase
2843
+ block = case subcommand
2844
+ when 'slots' then HashifyClusterSlots
2845
+ when 'nodes' then HashifyClusterNodes
2846
+ when 'slaves' then HashifyClusterSlaves
2847
+ when 'info' then HashifyInfo
2848
+ else Noop
2849
+ end
2850
+
2851
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
2852
+ block = Noop unless @cluster_mode
2853
+
2854
+ synchronize do |client|
2855
+ client.call([:cluster, subcommand] + args, &block)
2856
+ end
2857
+ end
2858
+
2859
+ # Sends `ASKING` command to random node and returns its reply.
2860
+ #
2861
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
2862
+ #
2863
+ # @return [String] `'OK'`
2864
+ def asking
2865
+ synchronize { |client| client.call(%i[asking]) }
2866
+ end
2867
+
2830
2868
  def id
2831
2869
  @original_client.id
2832
2870
  end
@@ -2840,6 +2878,8 @@ class Redis
2840
2878
  end
2841
2879
 
2842
2880
  def connection
2881
+ return @original_client.connection_info if @cluster_mode
2882
+
2843
2883
  {
2844
2884
  host: @original_client.host,
2845
2885
  port: @original_client.port,
@@ -2896,15 +2936,61 @@ private
2896
2936
 
2897
2937
  FloatifyPairs =
2898
2938
  lambda { |result|
2899
- if result.respond_to?(:each_slice)
2900
- result.each_slice(2).map do |member, score|
2901
- [member, Floatify.call(score)]
2902
- end
2903
- else
2904
- result
2939
+ result.each_slice(2).map do |member, score|
2940
+ [member, Floatify.call(score)]
2941
+ end
2942
+ }
2943
+
2944
+ HashifyInfo =
2945
+ lambda { |reply|
2946
+ Hash[reply.split("\r\n").map do |line|
2947
+ line.split(':', 2) unless line =~ /^(#|$)/
2948
+ end.compact]
2949
+ }
2950
+
2951
+ HashifyClusterNodeInfo =
2952
+ lambda { |str|
2953
+ arr = str.split(' ')
2954
+ {
2955
+ 'node_id' => arr[0],
2956
+ 'ip_port' => arr[1],
2957
+ 'flags' => arr[2].split(','),
2958
+ 'master_node_id' => arr[3],
2959
+ 'ping_sent' => arr[4],
2960
+ 'pong_recv' => arr[5],
2961
+ 'config_epoch' => arr[6],
2962
+ 'link_state' => arr[7],
2963
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
2964
+ }
2965
+ }
2966
+
2967
+ HashifyClusterSlots =
2968
+ lambda { |reply|
2969
+ reply.map do |arr|
2970
+ first_slot, last_slot = arr[0..1]
2971
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
2972
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
2973
+ {
2974
+ 'start_slot' => first_slot,
2975
+ 'end_slot' => last_slot,
2976
+ 'master' => master,
2977
+ 'replicas' => replicas
2978
+ }
2905
2979
  end
2906
2980
  }
2907
2981
 
2982
+ HashifyClusterNodes =
2983
+ lambda { |reply|
2984
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
2985
+ }
2986
+
2987
+ HashifyClusterSlaves =
2988
+ lambda { |reply|
2989
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
2990
+ }
2991
+
2992
+ Noop = ->(reply) { reply }
2993
+
2908
2994
  def _geoarguments(*args, options: nil, sort: nil, count: nil)
2909
2995
  args.push sort if sort
2910
2996
  args.push 'count', count if count
@@ -2927,11 +3013,11 @@ private
2927
3013
  @client = original
2928
3014
  end
2929
3015
  end
2930
-
2931
3016
  end
2932
3017
 
2933
3018
  require_relative "redis/version"
2934
3019
  require_relative "redis/connection"
2935
3020
  require_relative "redis/client"
3021
+ require_relative "redis/cluster"
2936
3022
  require_relative "redis/pipeline"
2937
3023
  require_relative "redis/subscribe"
@@ -18,6 +18,8 @@ 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
 
@@ -84,11 +86,14 @@ class Redis
84
86
 
85
87
  @pending_reads = 0
86
88
 
87
- if options.include?(:sentinels)
88
- @connector = Connector::Sentinel.new(@options)
89
- else
90
- @connector = Connector.new(@options)
91
- 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
92
97
  end
93
98
 
94
99
  def connect
@@ -186,13 +191,10 @@ class Redis
186
191
  exception = nil
187
192
 
188
193
  process(commands) do
189
- result[0] = read
190
-
191
- @reconnect = false
192
-
193
- (commands.size - 1).times do |i|
194
+ commands.size.times do |i|
194
195
  reply = read
195
- result[i + 1] = reply
196
+ result[i] = reply
197
+ @reconnect = false
196
198
  exception = reply if exception.nil? && reply.is_a?(CommandError)
197
199
  end
198
200
  end
@@ -372,6 +374,10 @@ class Redis
372
374
  disconnect
373
375
 
374
376
  if attempts <= @options[:reconnect_attempts] && @reconnect
377
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
378
+ @options[:reconnect_delay_max]].min
379
+
380
+ Kernel.sleep(sleep_t)
375
381
  retry
376
382
  else
377
383
  raise
@@ -449,6 +455,8 @@ class Redis
449
455
  options[:write_timeout] = Float(options[:write_timeout])
450
456
 
451
457
  options[:reconnect_attempts] = options[:reconnect_attempts].to_i
458
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
459
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
452
460
 
453
461
  options[:db] = options[:db].to_i
454
462
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
@@ -0,0 +1,285 @@
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 '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
+ assign_redirection_node(err.message).public_send(method_name, *args, &block)
219
+ elsif err.message.start_with?('ASK')
220
+ raise if retry_count <= 0
221
+ node = assign_asking_node(err.message)
222
+ node.call(%i[asking])
223
+ retry_count -= 1
224
+ retry
225
+ else
226
+ raise
227
+ end
228
+ end
229
+
230
+ def assign_redirection_node(err_msg)
231
+ _, slot, node_key = err_msg.split(' ')
232
+ slot = slot.to_i
233
+ @slot.put(slot, node_key)
234
+ find_node(node_key)
235
+ end
236
+
237
+ def assign_asking_node(err_msg)
238
+ _, _, node_key = err_msg.split(' ')
239
+ find_node(node_key)
240
+ end
241
+
242
+ def assign_node(command)
243
+ node_key = find_node_key(command)
244
+ find_node(node_key)
245
+ end
246
+
247
+ def find_node_key(command)
248
+ key = @command.extract_first_key(command)
249
+ return if key.empty?
250
+
251
+ slot = KeySlotConverter.convert(key)
252
+ return unless @slot.exists?(slot)
253
+
254
+ if @command.should_send_to_master?(command)
255
+ @slot.find_node_key_of_master(slot)
256
+ else
257
+ @slot.find_node_key_of_slave(slot)
258
+ end
259
+ end
260
+
261
+ def find_node(node_key)
262
+ return @node.sample if node_key.nil?
263
+ @node.find_by(node_key)
264
+ rescue Node::ReloadNeeded
265
+ update_cluster_info!(node_key)
266
+ @node.find_by(node_key)
267
+ end
268
+
269
+ def update_cluster_info!(node_key = nil)
270
+ unless node_key.nil?
271
+ host, port = NodeKey.split(node_key)
272
+ @option.add_node(host, port)
273
+ end
274
+
275
+ @node.map(&:disconnect)
276
+ @node, @slot = fetch_cluster_info!(@option)
277
+ end
278
+
279
+ def extract_keys_in_pipeline(pipeline)
280
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
281
+ command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
282
+ [node_keys, command_keys]
283
+ end
284
+ end
285
+ end