redis 3.0.0 → 4.2.2

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 (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +269 -0
  3. data/README.md +295 -58
  4. data/lib/redis.rb +1760 -451
  5. data/lib/redis/client.rb +355 -88
  6. data/lib/redis/cluster.rb +295 -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 +107 -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 +90 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +12 -8
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +232 -63
  21. data/lib/redis/connection/synchrony.rb +41 -14
  22. data/lib/redis/distributed.rb +205 -70
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +31 -73
  25. data/lib/redis/pipeline.rb +74 -18
  26. data/lib/redis/subscribe.rb +24 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +63 -160
  29. data/.gitignore +0 -10
  30. data/.order +0 -169
  31. data/.travis.yml +0 -50
  32. data/.travis/Gemfile +0 -11
  33. data/.yardopts +0 -3
  34. data/Rakefile +0 -392
  35. data/benchmarking/logging.rb +0 -62
  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/dist_redis.rb +0 -43
  42. data/examples/incr-decr.rb +0 -17
  43. data/examples/list.rb +0 -26
  44. data/examples/pubsub.rb +0 -31
  45. data/examples/sets.rb +0 -36
  46. data/examples/unicorn/config.ru +0 -3
  47. data/examples/unicorn/unicorn.rb +0 -20
  48. data/redis.gemspec +0 -41
  49. data/test/blocking_commands_test.rb +0 -42
  50. data/test/command_map_test.rb +0 -30
  51. data/test/commands_on_hashes_test.rb +0 -21
  52. data/test/commands_on_lists_test.rb +0 -20
  53. data/test/commands_on_sets_test.rb +0 -77
  54. data/test/commands_on_sorted_sets_test.rb +0 -109
  55. data/test/commands_on_strings_test.rb +0 -83
  56. data/test/commands_on_value_types_test.rb +0 -99
  57. data/test/connection_handling_test.rb +0 -189
  58. data/test/db/.gitignore +0 -1
  59. data/test/distributed_blocking_commands_test.rb +0 -46
  60. data/test/distributed_commands_on_hashes_test.rb +0 -10
  61. data/test/distributed_commands_on_lists_test.rb +0 -22
  62. data/test/distributed_commands_on_sets_test.rb +0 -83
  63. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  64. data/test/distributed_commands_on_strings_test.rb +0 -48
  65. data/test/distributed_commands_on_value_types_test.rb +0 -87
  66. data/test/distributed_commands_requiring_clustering_test.rb +0 -148
  67. data/test/distributed_connection_handling_test.rb +0 -23
  68. data/test/distributed_internals_test.rb +0 -15
  69. data/test/distributed_key_tags_test.rb +0 -52
  70. data/test/distributed_persistence_control_commands_test.rb +0 -26
  71. data/test/distributed_publish_subscribe_test.rb +0 -92
  72. data/test/distributed_remote_server_control_commands_test.rb +0 -53
  73. data/test/distributed_scripting_test.rb +0 -102
  74. data/test/distributed_sorting_test.rb +0 -20
  75. data/test/distributed_test.rb +0 -58
  76. data/test/distributed_transactions_test.rb +0 -32
  77. data/test/encoding_test.rb +0 -18
  78. data/test/error_replies_test.rb +0 -59
  79. data/test/helper.rb +0 -188
  80. data/test/helper_test.rb +0 -22
  81. data/test/internals_test.rb +0 -214
  82. data/test/lint/blocking_commands.rb +0 -124
  83. data/test/lint/hashes.rb +0 -162
  84. data/test/lint/lists.rb +0 -143
  85. data/test/lint/sets.rb +0 -96
  86. data/test/lint/sorted_sets.rb +0 -201
  87. data/test/lint/strings.rb +0 -157
  88. data/test/lint/value_types.rb +0 -106
  89. data/test/persistence_control_commands_test.rb +0 -26
  90. data/test/pipelining_commands_test.rb +0 -195
  91. data/test/publish_subscribe_test.rb +0 -153
  92. data/test/remote_server_control_commands_test.rb +0 -104
  93. data/test/scripting_test.rb +0 -78
  94. data/test/sorting_test.rb +0 -45
  95. data/test/support/connection/hiredis.rb +0 -1
  96. data/test/support/connection/ruby.rb +0 -1
  97. data/test/support/connection/synchrony.rb +0 -17
  98. data/test/support/redis_mock.rb +0 -92
  99. data/test/support/wire/synchrony.rb +0 -24
  100. data/test/support/wire/thread.rb +0 -5
  101. data/test/synchrony_driver.rb +0 -57
  102. data/test/test.conf +0 -9
  103. data/test/thread_safety_test.rb +0 -32
  104. data/test/transactions_test.rb +0 -244
  105. data/test/unknown_commands_test.rb +0 -14
  106. data/test/url_param_test.rb +0 -64
@@ -1,14 +1,23 @@
1
- require "redis/connection/command_helper"
2
- require "redis/connection/registry"
3
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "command_helper"
4
+ require_relative "registry"
5
+ require_relative "../errors"
4
6
  require "em-synchrony"
5
7
  require "hiredis/reader"
6
8
 
9
+ Kernel.warn(
10
+ "The redis synchrony driver is deprecated and will be removed in redis-rb 5.0. " \
11
+ "We're looking for people to maintain it as a separate gem, see https://github.com/redis/redis-rb/issues/915"
12
+ )
13
+
7
14
  class Redis
8
15
  module Connection
9
16
  class RedisClient < EventMachine::Connection
10
17
  include EventMachine::Deferrable
11
18
 
19
+ attr_accessor :timeout
20
+
12
21
  def post_init
13
22
  @req = nil
14
23
  @connected = false
@@ -27,18 +36,24 @@ class Redis
27
36
  def receive_data(data)
28
37
  @reader.feed(data)
29
38
 
30
- begin
31
- until (reply = @reader.gets) == false
32
- reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
33
- @req.succeed [:reply, reply]
39
+ loop do
40
+ begin
41
+ reply = @reader.gets
42
+ rescue RuntimeError => err
43
+ @req.fail [:error, ProtocolError.new(err.message)]
44
+ break
34
45
  end
35
- rescue RuntimeError => err
36
- @req.fail [:error, ProtocolError.new(err.message)]
46
+
47
+ break if reply == false
48
+
49
+ reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
50
+ @req.succeed [:reply, reply]
37
51
  end
38
52
  end
39
53
 
40
54
  def read
41
55
  @req = EventMachine::DefaultDeferrable.new
56
+ @req.timeout(@timeout, :timeout) if @timeout > 0
42
57
  EventMachine::Synchrony.sync @req
43
58
  end
44
59
 
@@ -62,10 +77,20 @@ class Redis
62
77
 
63
78
  def self.connect(config)
64
79
  if config[:scheme] == "unix"
65
- conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
80
+ begin
81
+ conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
82
+ rescue RuntimeError => e
83
+ if e.message == "no connection"
84
+ raise Errno::ECONNREFUSED
85
+ else
86
+ raise e
87
+ end
88
+ end
89
+ elsif config[:scheme] == "rediss" || config[:ssl]
90
+ raise NotImplementedError, "SSL not supported by synchrony driver"
66
91
  else
67
92
  conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c|
68
- c.pending_connect_timeout = [config[:timeout], 0.1].max
93
+ c.pending_connect_timeout = [config[:connect_timeout], 0.1].max
69
94
  end
70
95
  end
71
96
 
@@ -76,7 +101,7 @@ class Redis
76
101
  raise Errno::ECONNREFUSED if Fiber.yield == :refused
77
102
 
78
103
  instance = new(conn)
79
- instance.timeout = config[:timeout]
104
+ instance.timeout = config[:read_timeout]
80
105
  instance
81
106
  end
82
107
 
@@ -85,11 +110,11 @@ class Redis
85
110
  end
86
111
 
87
112
  def connected?
88
- @connection && @connection.connected?
113
+ @connection&.connected?
89
114
  end
90
115
 
91
116
  def timeout=(timeout)
92
- @timeout = timeout
117
+ @connection.timeout = timeout
93
118
  end
94
119
 
95
120
  def disconnect
@@ -108,6 +133,8 @@ class Redis
108
133
  payload
109
134
  elsif type == :error
110
135
  raise payload
136
+ elsif type == :timeout
137
+ raise TimeoutError
111
138
  else
112
139
  raise "Unknown type #{type.inspect}"
113
140
  end
@@ -1,37 +1,47 @@
1
- require "redis/hash_ring"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "hash_ring"
2
4
 
3
5
  class Redis
4
6
  class Distributed
5
-
6
7
  class CannotDistribute < RuntimeError
7
8
  def initialize(command)
8
9
  @command = command
9
10
  end
10
11
 
11
12
  def message
12
- "#{@command.to_s.upcase} cannot be used in Redis::Distributed because the keys involved need to be on the same server or because we cannot guarantee that the operation will be atomic."
13
+ "#{@command.to_s.upcase} cannot be used in Redis::Distributed because the keys involved need " \
14
+ "to be on the same server or because we cannot guarantee that the operation will be atomic."
13
15
  end
14
16
  end
15
17
 
16
18
  attr_reader :ring
17
19
 
18
- def initialize(urls, options = {})
19
- @tag = options.delete(:tag) || /^\{(.+?)\}/
20
- @default_options = options
21
- @ring = HashRing.new urls.map { |url| Redis.new(options.merge(:url => url)) }
20
+ def initialize(node_configs, options = {})
21
+ @tag = options[:tag] || /^\{(.+?)\}/
22
+ @ring = options[:ring] || HashRing.new
23
+ @node_configs = node_configs.dup
24
+ @default_options = options.dup
25
+ node_configs.each { |node_config| add_node(node_config) }
22
26
  @subscribed_node = nil
27
+ @watch_key = nil
23
28
  end
24
29
 
25
30
  def node_for(key)
26
- @ring.get_node(key_tag(key.to_s) || key.to_s)
31
+ key = key_tag(key.to_s) || key.to_s
32
+ raise CannotDistribute, :watch if @watch_key && @watch_key != key
33
+
34
+ @ring.get_node(key)
27
35
  end
28
36
 
29
37
  def nodes
30
38
  @ring.nodes
31
39
  end
32
40
 
33
- def add_node(url)
34
- @ring.add_node Redis.new(@default_options.merge(:url => url))
41
+ def add_node(options)
42
+ options = { url: options } if options.is_a?(String)
43
+ options = @default_options.merge(options)
44
+ @ring.add_node Redis.new(options)
35
45
  end
36
46
 
37
47
  # Change the selected database for the current connection.
@@ -134,6 +144,21 @@ class Redis
134
144
  node_for(key).pttl(key)
135
145
  end
136
146
 
147
+ # Return a serialized version of the value stored at a key.
148
+ def dump(key)
149
+ node_for(key).dump(key)
150
+ end
151
+
152
+ # Create a key using the serialized value, previously obtained using DUMP.
153
+ def restore(key, ttl, serialized_value, **options)
154
+ node_for(key).restore(key, ttl, serialized_value, **options)
155
+ end
156
+
157
+ # Transfer a key from the connected instance to another instance.
158
+ def migrate(_key, _options)
159
+ raise CannotDistribute, :migrate
160
+ end
161
+
137
162
  # Delete a key.
138
163
  def del(*args)
139
164
  keys_per_node = args.group_by { |key| node_for(key) }
@@ -142,9 +167,42 @@ class Redis
142
167
  end
143
168
  end
144
169
 
170
+ # Unlink keys.
171
+ def unlink(*args)
172
+ keys_per_node = args.group_by { |key| node_for(key) }
173
+ keys_per_node.inject(0) do |sum, (node, keys)|
174
+ sum + node.unlink(*keys)
175
+ end
176
+ end
177
+
145
178
  # Determine if a key exists.
146
- def exists(key)
147
- node_for(key).exists(key)
179
+ def exists(*args)
180
+ if !Redis.exists_returns_integer && args.size == 1
181
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3, if you want to keep the old behavior, " \
182
+ "use `exists?` instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = true. " \
183
+ "(#{::Kernel.caller(1, 1).first})\n"
184
+
185
+ if defined?(::Warning)
186
+ ::Warning.warn(message)
187
+ else
188
+ warn(message)
189
+ end
190
+ exists?(*args)
191
+ else
192
+ keys_per_node = args.group_by { |key| node_for(key) }
193
+ keys_per_node.inject(0) do |sum, (node, keys)|
194
+ sum + node._exists(*keys)
195
+ end
196
+ end
197
+ end
198
+
199
+ # Determine if any of the keys exists.
200
+ def exists?(*args)
201
+ keys_per_node = args.group_by { |key| node_for(key) }
202
+ keys_per_node.each do |node, keys|
203
+ return true if node.exists?(*keys)
204
+ end
205
+ false
148
206
  end
149
207
 
150
208
  # Find all keys matching the given pattern.
@@ -177,11 +235,11 @@ class Redis
177
235
  end
178
236
 
179
237
  # Sort the elements in a list, set or sorted set.
180
- def sort(key, options = {})
238
+ def sort(key, **options)
181
239
  keys = [key, options[:by], options[:store], *Array(options[:get])].compact
182
240
 
183
241
  ensure_same_node(:sort, keys) do |node|
184
- node.sort(key, options)
242
+ node.sort(key, **options)
185
243
  end
186
244
  end
187
245
 
@@ -216,8 +274,8 @@ class Redis
216
274
  end
217
275
 
218
276
  # Set the string value of a key.
219
- def set(key, value)
220
- node_for(key).set(key, value)
277
+ def set(key, value, **options)
278
+ node_for(key).set(key, value, **options)
221
279
  end
222
280
 
223
281
  # Set the time to live in seconds of a key.
@@ -236,20 +294,20 @@ class Redis
236
294
  end
237
295
 
238
296
  # Set multiple keys to multiple values.
239
- def mset(*args)
297
+ def mset(*_args)
240
298
  raise CannotDistribute, :mset
241
299
  end
242
300
 
243
- def mapped_mset(hash)
301
+ def mapped_mset(_hash)
244
302
  raise CannotDistribute, :mapped_mset
245
303
  end
246
304
 
247
305
  # Set multiple keys to multiple values, only if none of the keys exist.
248
- def msetnx(*args)
306
+ def msetnx(*_args)
249
307
  raise CannotDistribute, :msetnx
250
308
  end
251
309
 
252
- def mapped_msetnx(hash)
310
+ def mapped_msetnx(_hash)
253
311
  raise CannotDistribute, :mapped_msetnx
254
312
  end
255
313
 
@@ -258,13 +316,16 @@ class Redis
258
316
  node_for(key).get(key)
259
317
  end
260
318
 
261
- # Get the values of all the given keys.
319
+ # Get the values of all the given keys as an Array.
262
320
  def mget(*keys)
263
- raise CannotDistribute, :mget
321
+ mapped_mget(*keys).values_at(*keys)
264
322
  end
265
323
 
324
+ # Get the values of all the given keys as a Hash.
266
325
  def mapped_mget(*keys)
267
- raise CannotDistribute, :mapped_mget
326
+ keys.group_by { |k| node_for k }.inject({}) do |results, (node, subkeys)|
327
+ results.merge! node.mapped_mget(*subkeys)
328
+ end
268
329
  end
269
330
 
270
331
  # Overwrite part of a string at key starting at the specified offset.
@@ -292,6 +353,23 @@ class Redis
292
353
  node_for(key).append(key, value)
293
354
  end
294
355
 
356
+ # Count the number of set bits in a range of the string value stored at key.
357
+ def bitcount(key, start = 0, stop = -1)
358
+ node_for(key).bitcount(key, start, stop)
359
+ end
360
+
361
+ # Perform a bitwise operation between strings and store the resulting string in a key.
362
+ def bitop(operation, destkey, *keys)
363
+ ensure_same_node(:bitop, [destkey] + keys) do |node|
364
+ node.bitop(operation, destkey, *keys)
365
+ end
366
+ end
367
+
368
+ # Return the position of the first bit set to 1 or 0 in a string.
369
+ def bitpos(key, bit, start = nil, stop = nil)
370
+ node_for(key).bitpos(key, bit, start, stop)
371
+ end
372
+
295
373
  # Set the string value of a key and return its old value.
296
374
  def getset(key, value)
297
375
  node_for(key).getset(key, value)
@@ -306,7 +384,7 @@ class Redis
306
384
  get(key)
307
385
  end
308
386
 
309
- def []=(key,value)
387
+ def []=(key, value)
310
388
  set(key, value)
311
389
  end
312
390
 
@@ -354,14 +432,12 @@ class Redis
354
432
  end
355
433
 
356
434
  def _bpop(cmd, args)
357
- options = {}
358
-
359
- case args.last
360
- when Hash
435
+ timeout = if args.last.is_a?(Hash)
361
436
  options = args.pop
362
- when Integer
437
+ options[:timeout]
438
+ elsif args.last.respond_to?(:to_int)
363
439
  # Issue deprecation notice in obnoxious mode...
364
- options[:timeout] = args.pop
440
+ args.pop.to_int
365
441
  end
366
442
 
367
443
  if args.size > 1
@@ -371,7 +447,11 @@ class Redis
371
447
  keys = args.flatten
372
448
 
373
449
  ensure_same_node(cmd, keys) do |node|
374
- node.__send__(cmd, keys, options)
450
+ if timeout
451
+ node.__send__(cmd, keys, timeout: timeout)
452
+ else
453
+ node.__send__(cmd, keys)
454
+ end
375
455
  end
376
456
  end
377
457
 
@@ -389,15 +469,9 @@ class Redis
389
469
 
390
470
  # Pop a value from a list, push it to another list and return it; or block
391
471
  # until one is available.
392
- def brpoplpush(source, destination, options = {})
393
- case options
394
- when Integer
395
- # Issue deprecation notice in obnoxious mode...
396
- options = { :timeout => options }
397
- end
398
-
472
+ def brpoplpush(source, destination, deprecated_timeout = 0, **options)
399
473
  ensure_same_node(:brpoplpush, [source, destination]) do |node|
400
- node.brpoplpush(source, destination, options)
474
+ node.brpoplpush(source, destination, deprecated_timeout, **options)
401
475
  end
402
476
  end
403
477
 
@@ -447,13 +521,13 @@ class Redis
447
521
  end
448
522
 
449
523
  # Remove and return a random member from a set.
450
- def spop(key)
451
- node_for(key).spop(key)
524
+ def spop(key, count = nil)
525
+ node_for(key).spop(key, count)
452
526
  end
453
527
 
454
528
  # Get a random member from a set.
455
- def srandmember(key)
456
- node_for(key).srandmember(key)
529
+ def srandmember(key, count = nil)
530
+ node_for(key).srandmember(key, count)
457
531
  end
458
532
 
459
533
  # Move a member from one set to another.
@@ -473,6 +547,16 @@ class Redis
473
547
  node_for(key).smembers(key)
474
548
  end
475
549
 
550
+ # Scan a set
551
+ def sscan(key, cursor, **options)
552
+ node_for(key).sscan(key, cursor, **options)
553
+ end
554
+
555
+ # Scan a set and return an enumerator
556
+ def sscan_each(key, **options, &block)
557
+ node_for(key).sscan_each(key, **options, &block)
558
+ end
559
+
476
560
  # Subtract multiple sets.
477
561
  def sdiff(*keys)
478
562
  ensure_same_node(:sdiff, keys) do |node|
@@ -525,6 +609,7 @@ class Redis
525
609
  def zadd(key, *args)
526
610
  node_for(key).zadd(key, *args)
527
611
  end
612
+ ruby2_keywords(:zadd) if respond_to?(:ruby2_keywords, true)
528
613
 
529
614
  # Increment the score of a member in a sorted set.
530
615
  def zincrby(key, increment, member)
@@ -542,14 +627,14 @@ class Redis
542
627
  end
543
628
 
544
629
  # Return a range of members in a sorted set, by index.
545
- def zrange(key, start, stop, options = {})
546
- node_for(key).zrange(key, start, stop, options)
630
+ def zrange(key, start, stop, **options)
631
+ node_for(key).zrange(key, start, stop, **options)
547
632
  end
548
633
 
549
634
  # Return a range of members in a sorted set, by index, with scores ordered
550
635
  # from high to low.
551
- def zrevrange(key, start, stop, options = {})
552
- node_for(key).zrevrange(key, start, stop, options)
636
+ def zrevrange(key, start, stop, **options)
637
+ node_for(key).zrevrange(key, start, stop, **options)
553
638
  end
554
639
 
555
640
  # Determine the index of a member in a sorted set.
@@ -569,14 +654,14 @@ class Redis
569
654
  end
570
655
 
571
656
  # Return a range of members in a sorted set, by score.
572
- def zrangebyscore(key, min, max, options = {})
573
- node_for(key).zrangebyscore(key, min, max, options)
657
+ def zrangebyscore(key, min, max, **options)
658
+ node_for(key).zrangebyscore(key, min, max, **options)
574
659
  end
575
660
 
576
661
  # Return a range of members in a sorted set, by score, with scores ordered
577
662
  # from high to low.
578
- def zrevrangebyscore(key, max, min, options = {})
579
- node_for(key).zrevrangebyscore(key, max, min, options)
663
+ def zrevrangebyscore(key, max, min, **options)
664
+ node_for(key).zrevrangebyscore(key, max, min, **options)
580
665
  end
581
666
 
582
667
  # Remove all members in a sorted set within the given scores.
@@ -591,16 +676,16 @@ class Redis
591
676
 
592
677
  # Intersect multiple sorted sets and store the resulting sorted set in a new
593
678
  # key.
594
- def zinterstore(destination, keys, options = {})
679
+ def zinterstore(destination, keys, **options)
595
680
  ensure_same_node(:zinterstore, [destination] + keys) do |node|
596
- node.zinterstore(destination, keys, options)
681
+ node.zinterstore(destination, keys, **options)
597
682
  end
598
683
  end
599
684
 
600
685
  # Add multiple sorted sets and store the resulting sorted set in a new key.
601
- def zunionstore(destination, keys, options = {})
686
+ def zunionstore(destination, keys, **options)
602
687
  ensure_same_node(:zunionstore, [destination] + keys) do |node|
603
- node.zunionstore(destination, keys, options)
688
+ node.zunionstore(destination, keys, **options)
604
689
  end
605
690
  end
606
691
 
@@ -609,9 +694,9 @@ class Redis
609
694
  node_for(key).hlen(key)
610
695
  end
611
696
 
612
- # Set the string value of a hash field.
613
- def hset(key, field, value)
614
- node_for(key).hset(key, field, value)
697
+ # Set multiple hash fields to multiple values.
698
+ def hset(key, *attrs)
699
+ node_for(key).hset(key, *attrs)
615
700
  end
616
701
 
617
702
  # Set the value of a hash field, only if the field does not exist.
@@ -643,8 +728,8 @@ class Redis
643
728
  end
644
729
 
645
730
  # Delete one or more hash fields.
646
- def hdel(key, field)
647
- node_for(key).hdel(key, field)
731
+ def hdel(key, *fields)
732
+ node_for(key).hdel(key, *fields)
648
733
  end
649
734
 
650
735
  # Determine if a hash field exists.
@@ -683,7 +768,7 @@ class Redis
683
768
  end
684
769
 
685
770
  def subscribed?
686
- !! @subscribed_node
771
+ !!@subscribed_node
687
772
  end
688
773
 
689
774
  # Listen for messages published to the given channels.
@@ -701,7 +786,8 @@ class Redis
701
786
 
702
787
  # Stop listening for messages posted to the given channels.
703
788
  def unsubscribe(*channels)
704
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
789
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
790
+
705
791
  @subscribed_node.unsubscribe(*channels)
706
792
  end
707
793
 
@@ -717,13 +803,26 @@ class Redis
717
803
  end
718
804
 
719
805
  # Watch the given keys to determine execution of the MULTI/EXEC block.
720
- def watch(*keys)
721
- raise CannotDistribute, :watch
806
+ def watch(*keys, &block)
807
+ ensure_same_node(:watch, keys) do |node|
808
+ @watch_key = key_tag(keys.first) || keys.first.to_s
809
+
810
+ begin
811
+ node.watch(*keys, &block)
812
+ rescue StandardError
813
+ @watch_key = nil
814
+ raise
815
+ end
816
+ end
722
817
  end
723
818
 
724
819
  # Forget about all watched keys.
725
820
  def unwatch
726
- raise CannotDistribute, :unwatch
821
+ raise CannotDistribute, :unwatch unless @watch_key
822
+
823
+ result = node_for(@watch_key).unwatch
824
+ @watch_key = nil
825
+ result
727
826
  end
728
827
 
729
828
  def pipelined
@@ -731,18 +830,30 @@ class Redis
731
830
  end
732
831
 
733
832
  # Mark the start of a transaction block.
734
- def multi
735
- raise CannotDistribute, :multi
833
+ def multi(&block)
834
+ raise CannotDistribute, :multi unless @watch_key
835
+
836
+ result = node_for(@watch_key).multi(&block)
837
+ @watch_key = nil if block_given?
838
+ result
736
839
  end
737
840
 
738
841
  # Execute all commands issued after MULTI.
739
842
  def exec
740
- raise CannotDistribute, :exec
843
+ raise CannotDistribute, :exec unless @watch_key
844
+
845
+ result = node_for(@watch_key).exec
846
+ @watch_key = nil
847
+ result
741
848
  end
742
849
 
743
850
  # Discard all commands issued after MULTI.
744
851
  def discard
745
- raise CannotDistribute, :discard
852
+ raise CannotDistribute, :discard unless @watch_key
853
+
854
+ result = node_for(@watch_key).discard
855
+ @watch_key = nil
856
+ result
746
857
  end
747
858
 
748
859
  # Control remote script registry.
@@ -750,6 +861,26 @@ class Redis
750
861
  on_each_node(:script, subcommand, *args)
751
862
  end
752
863
 
864
+ # Add one or more members to a HyperLogLog structure.
865
+ def pfadd(key, member)
866
+ node_for(key).pfadd(key, member)
867
+ end
868
+
869
+ # Get the approximate cardinality of members added to HyperLogLog structure.
870
+ def pfcount(*keys)
871
+ ensure_same_node(:pfcount, keys.flatten(1)) do |node|
872
+ node.pfcount(keys)
873
+ end
874
+ end
875
+
876
+ # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of
877
+ # the observed Sets of the source HyperLogLog structures.
878
+ def pfmerge(dest_key, *source_key)
879
+ ensure_same_node(:pfmerge, [dest_key, *source_key]) do |node|
880
+ node.pfmerge(dest_key, *source_key)
881
+ end
882
+ end
883
+
753
884
  def _eval(cmd, args)
754
885
  script = args.shift
755
886
  options = args.pop if args.last.is_a?(Hash)
@@ -777,7 +908,11 @@ class Redis
777
908
  "#<Redis client v#{Redis::VERSION} for #{nodes.map(&:id).join(', ')}>"
778
909
  end
779
910
 
780
- protected
911
+ def dup
912
+ self.class.new(@node_configs, @default_options)
913
+ end
914
+
915
+ protected
781
916
 
782
917
  def on_each_node(command, *args)
783
918
  nodes.map do |node|