redis 4.0.2 → 4.0.3

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