redis 4.0.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (124) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +46 -1
  4. data/lib/redis/client.rb +29 -12
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +34 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +104 -0
  9. data/lib/redis/cluster/node_key.rb +35 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +77 -0
  12. data/lib/redis/cluster/slot.rb +69 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +286 -0
  15. data/lib/redis/connection/ruby.rb +5 -2
  16. data/lib/redis/distributed.rb +13 -6
  17. data/lib/redis/errors.rb +46 -0
  18. data/lib/redis/pipeline.rb +9 -1
  19. data/lib/redis/version.rb +1 -1
  20. data/lib/redis.rb +692 -25
  21. metadata +27 -184
  22. data/.gitignore +0 -16
  23. data/.travis/Gemfile +0 -13
  24. data/.travis.yml +0 -73
  25. data/.yardopts +0 -3
  26. data/Gemfile +0 -3
  27. data/benchmarking/logging.rb +0 -71
  28. data/benchmarking/pipeline.rb +0 -51
  29. data/benchmarking/speed.rb +0 -21
  30. data/benchmarking/suite.rb +0 -24
  31. data/benchmarking/worker.rb +0 -71
  32. data/bors.toml +0 -14
  33. data/examples/basic.rb +0 -15
  34. data/examples/consistency.rb +0 -114
  35. data/examples/dist_redis.rb +0 -43
  36. data/examples/incr-decr.rb +0 -17
  37. data/examples/list.rb +0 -26
  38. data/examples/pubsub.rb +0 -37
  39. data/examples/sentinel/sentinel.conf +0 -9
  40. data/examples/sentinel/start +0 -49
  41. data/examples/sentinel.rb +0 -41
  42. data/examples/sets.rb +0 -36
  43. data/examples/unicorn/config.ru +0 -3
  44. data/examples/unicorn/unicorn.rb +0 -20
  45. data/makefile +0 -42
  46. data/redis.gemspec +0 -42
  47. data/test/bitpos_test.rb +0 -63
  48. data/test/blocking_commands_test.rb +0 -40
  49. data/test/client_test.rb +0 -59
  50. data/test/command_map_test.rb +0 -28
  51. data/test/commands_on_hashes_test.rb +0 -19
  52. data/test/commands_on_hyper_log_log_test.rb +0 -19
  53. data/test/commands_on_lists_test.rb +0 -18
  54. data/test/commands_on_sets_test.rb +0 -75
  55. data/test/commands_on_sorted_sets_test.rb +0 -150
  56. data/test/commands_on_strings_test.rb +0 -99
  57. data/test/commands_on_value_types_test.rb +0 -171
  58. data/test/connection_handling_test.rb +0 -275
  59. data/test/connection_test.rb +0 -57
  60. data/test/db/.gitkeep +0 -0
  61. data/test/distributed_blocking_commands_test.rb +0 -44
  62. data/test/distributed_commands_on_hashes_test.rb +0 -8
  63. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -31
  64. data/test/distributed_commands_on_lists_test.rb +0 -20
  65. data/test/distributed_commands_on_sets_test.rb +0 -106
  66. data/test/distributed_commands_on_sorted_sets_test.rb +0 -16
  67. data/test/distributed_commands_on_strings_test.rb +0 -69
  68. data/test/distributed_commands_on_value_types_test.rb +0 -93
  69. data/test/distributed_commands_requiring_clustering_test.rb +0 -162
  70. data/test/distributed_connection_handling_test.rb +0 -21
  71. data/test/distributed_internals_test.rb +0 -68
  72. data/test/distributed_key_tags_test.rb +0 -50
  73. data/test/distributed_persistence_control_commands_test.rb +0 -24
  74. data/test/distributed_publish_subscribe_test.rb +0 -90
  75. data/test/distributed_remote_server_control_commands_test.rb +0 -64
  76. data/test/distributed_scripting_test.rb +0 -100
  77. data/test/distributed_sorting_test.rb +0 -18
  78. data/test/distributed_test.rb +0 -56
  79. data/test/distributed_transactions_test.rb +0 -30
  80. data/test/encoding_test.rb +0 -14
  81. data/test/error_replies_test.rb +0 -57
  82. data/test/fork_safety_test.rb +0 -60
  83. data/test/helper.rb +0 -201
  84. data/test/helper_test.rb +0 -22
  85. data/test/internals_test.rb +0 -389
  86. data/test/lint/blocking_commands.rb +0 -150
  87. data/test/lint/hashes.rb +0 -162
  88. data/test/lint/hyper_log_log.rb +0 -60
  89. data/test/lint/lists.rb +0 -143
  90. data/test/lint/sets.rb +0 -140
  91. data/test/lint/sorted_sets.rb +0 -316
  92. data/test/lint/strings.rb +0 -246
  93. data/test/lint/value_types.rb +0 -130
  94. data/test/persistence_control_commands_test.rb +0 -24
  95. data/test/pipelining_commands_test.rb +0 -238
  96. data/test/publish_subscribe_test.rb +0 -280
  97. data/test/remote_server_control_commands_test.rb +0 -175
  98. data/test/scanning_test.rb +0 -407
  99. data/test/scripting_test.rb +0 -76
  100. data/test/sentinel_command_test.rb +0 -78
  101. data/test/sentinel_test.rb +0 -253
  102. data/test/sorting_test.rb +0 -57
  103. data/test/ssl_test.rb +0 -69
  104. data/test/support/connection/hiredis.rb +0 -1
  105. data/test/support/connection/ruby.rb +0 -1
  106. data/test/support/connection/synchrony.rb +0 -17
  107. data/test/support/redis_mock.rb +0 -130
  108. data/test/support/ssl/gen_certs.sh +0 -31
  109. data/test/support/ssl/trusted-ca.crt +0 -25
  110. data/test/support/ssl/trusted-ca.key +0 -27
  111. data/test/support/ssl/trusted-cert.crt +0 -81
  112. data/test/support/ssl/trusted-cert.key +0 -28
  113. data/test/support/ssl/untrusted-ca.crt +0 -26
  114. data/test/support/ssl/untrusted-ca.key +0 -27
  115. data/test/support/ssl/untrusted-cert.crt +0 -82
  116. data/test/support/ssl/untrusted-cert.key +0 -28
  117. data/test/support/wire/synchrony.rb +0 -24
  118. data/test/support/wire/thread.rb +0 -5
  119. data/test/synchrony_driver.rb +0 -85
  120. data/test/test.conf.erb +0 -9
  121. data/test/thread_safety_test.rb +0 -60
  122. data/test/transactions_test.rb +0 -262
  123. data/test/unknown_commands_test.rb +0 -12
  124. data/test/url_param_test.rb +0 -136
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep slot and node key map for Redis Cluster Client
8
+ class Slot
9
+ ROLE_SLAVE = 'slave'
10
+
11
+ def initialize(available_slots, node_flags = {}, with_replica = false)
12
+ @with_replica = with_replica
13
+ @node_flags = node_flags
14
+ @map = build_slot_node_key_map(available_slots)
15
+ end
16
+
17
+ def exists?(slot)
18
+ @map.key?(slot)
19
+ end
20
+
21
+ def find_node_key_of_master(slot)
22
+ return nil unless exists?(slot)
23
+
24
+ @map[slot][:master]
25
+ end
26
+
27
+ def find_node_key_of_slave(slot)
28
+ return nil unless exists?(slot)
29
+ return find_node_key_of_master(slot) if replica_disabled?
30
+
31
+ @map[slot][:slaves].to_a.sample
32
+ end
33
+
34
+ def put(slot, node_key)
35
+ assign_node_key(@map, slot, node_key)
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ def replica_disabled?
42
+ !@with_replica
43
+ end
44
+
45
+ def master?(node_key)
46
+ !slave?(node_key)
47
+ end
48
+
49
+ def slave?(node_key)
50
+ @node_flags[node_key] == ROLE_SLAVE
51
+ end
52
+
53
+ def build_slot_node_key_map(available_slots)
54
+ available_slots.each_with_object({}) do |(node_key, slots), acc|
55
+ slots.each { |slot| assign_node_key(acc, slot, node_key) }
56
+ end
57
+ end
58
+
59
+ def assign_node_key(mappings, slot, node_key)
60
+ mappings[slot] ||= { master: nil, slaves: Set.new }
61
+ if master?(node_key)
62
+ mappings[slot][:master] = node_key
63
+ else
64
+ mappings[slot][:slaves].add(node_key)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+ require_relative 'node_key'
5
+
6
+ class Redis
7
+ class Cluster
8
+ # Load and hashify slot info for Redis Cluster Client
9
+ module SlotLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ info = {}
14
+
15
+ nodes.each do |node|
16
+ info = Hash[*fetch_slot_info(node)]
17
+ info.empty? ? next : break
18
+ end
19
+
20
+ return info unless info.empty?
21
+
22
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
23
+ end
24
+
25
+ def fetch_slot_info(node)
26
+ node.call(%i[cluster slots])
27
+ .map { |arr| parse_slot_info(arr, default_ip: node.host) }
28
+ .flatten
29
+ rescue CannotConnectError, ConnectionError, CommandError
30
+ {} # can retry on another node
31
+ end
32
+
33
+ def parse_slot_info(arr, default_ip:)
34
+ first_slot, last_slot = arr[0..1]
35
+ slot_range = (first_slot..last_slot).freeze
36
+ arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
37
+ .flatten
38
+ end
39
+
40
+ def stringify_node_key(arr, default_ip)
41
+ ip, port = arr
42
+ ip = default_ip if ip.empty? # When cluster is down
43
+ NodeKey.build_from_host_port(ip, port)
44
+ end
45
+
46
+ private_class_method :fetch_slot_info, :parse_slot_info, :stringify_node_key
47
+ end
48
+ end
49
+ end
@@ -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
@@ -267,7 +267,10 @@ class Redis
267
267
  ssl_sock = new(tcp_sock, ctx)
268
268
  ssl_sock.hostname = host
269
269
  ssl_sock.connect
270
- ssl_sock.post_connection_check(host)
270
+
271
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE
272
+ ssl_sock.post_connection_check(host)
273
+ end
271
274
 
272
275
  ssl_sock
273
276
  end
@@ -294,7 +297,7 @@ class Redis
294
297
  end
295
298
 
296
299
  instance = new(sock)
297
- instance.timeout = config[:timeout]
300
+ instance.timeout = config[:read_timeout]
298
301
  instance.write_timeout = config[:write_timeout]
299
302
  instance.set_tcp_keepalive config[:tcp_keepalive]
300
303
  instance
@@ -161,6 +161,14 @@ class Redis
161
161
  end
162
162
  end
163
163
 
164
+ # Unlink keys.
165
+ def unlink(*args)
166
+ keys_per_node = args.group_by { |key| node_for(key) }
167
+ keys_per_node.inject(0) do |sum, (node, keys)|
168
+ sum + node.unlink(*keys)
169
+ end
170
+ end
171
+
164
172
  # Determine if a key exists.
165
173
  def exists(key)
166
174
  node_for(key).exists(key)
@@ -395,12 +403,11 @@ class Redis
395
403
  def _bpop(cmd, args)
396
404
  options = {}
397
405
 
398
- case args.last
399
- when Hash
406
+ if args.last.is_a?(Hash)
400
407
  options = args.pop
401
- when Integer
408
+ elsif args.last.respond_to?(:to_int)
402
409
  # Issue deprecation notice in obnoxious mode...
403
- options[:timeout] = args.pop
410
+ options[:timeout] = args.pop.to_int
404
411
  end
405
412
 
406
413
  if args.size > 1
@@ -692,8 +699,8 @@ class Redis
692
699
  end
693
700
 
694
701
  # Delete one or more hash fields.
695
- def hdel(key, field)
696
- node_for(key).hdel(key, field)
702
+ def hdel(key, *fields)
703
+ node_for(key).hdel(key, *fields)
697
704
  end
698
705
 
699
706
  # Determine if a hash field exists.
data/lib/redis/errors.rb CHANGED
@@ -37,4 +37,50 @@ class Redis
37
37
  # Raised when the connection was inherited by a child process.
38
38
  class InheritedError < BaseConnectionError
39
39
  end
40
+
41
+ # Raised when client options are invalid.
42
+ class InvalidClientOptionError < BaseError
43
+ end
44
+
45
+ class Cluster
46
+ # Raised when client connected to redis as cluster mode
47
+ # and some cluster subcommands were called.
48
+ class OrchestrationCommandNotSupported < BaseError
49
+ def initialize(command, subcommand = '')
50
+ str = [command, subcommand].map(&:to_s).reject(&:empty?).join(' ').upcase
51
+ msg = "#{str} command should be used with care "\
52
+ 'only by applications orchestrating Redis Cluster, like redis-trib, '\
53
+ 'and the command if used out of the right context can leave the cluster '\
54
+ 'in a wrong state or cause data loss.'
55
+ super(msg)
56
+ end
57
+ end
58
+
59
+ # Raised when error occurs on any node of cluster.
60
+ class CommandErrorCollection < BaseError
61
+ attr_reader :errors
62
+
63
+ # @param errors [Hash{String => Redis::CommandError}]
64
+ # @param error_message [String]
65
+ def initialize(errors, error_message = 'Command errors were replied on any node')
66
+ @errors = errors
67
+ super(error_message)
68
+ end
69
+ end
70
+
71
+ # Raised when cluster client can't select node.
72
+ class AmbiguousNodeError < BaseError
73
+ def initialize(command)
74
+ super("Cluster client doesn't know which node the #{command} command should be sent to.")
75
+ end
76
+ end
77
+
78
+ # Raised when commands in pipelining include cross slot keys.
79
+ class CrossSlotPipeliningError < BaseError
80
+ def initialize(keys)
81
+ super("Cluster client couldn't send pipelining to single node. "\
82
+ "The commands include cross slot keys. #{keys}")
83
+ end
84
+ end
85
+ end
40
86
  end
@@ -22,6 +22,10 @@ class Redis
22
22
  @shutdown
23
23
  end
24
24
 
25
+ def empty?
26
+ @futures.empty?
27
+ end
28
+
25
29
  def call(command, &block)
26
30
  # A pipeline that contains a shutdown should not raise ECONNRESET when
27
31
  # the connection is gone.
@@ -86,7 +90,11 @@ class Redis
86
90
  end
87
91
 
88
92
  def commands
89
- [[:multi]] + super + [[:exec]]
93
+ if empty?
94
+ []
95
+ else
96
+ [[:multi]] + super + [[:exec]]
97
+ end
90
98
  end
91
99
  end
92
100
  end
data/lib/redis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "4.0.1"
2
+ VERSION = '4.1.0'
3
3
  end