redis 4.0.1 → 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 (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