redis 3.2.2 → 4.4.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 (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +175 -13
  3. data/README.md +223 -76
  4. data/lib/redis.rb +1360 -445
  5. data/lib/redis/client.rb +183 -103
  6. data/lib/redis/cluster.rb +291 -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 +108 -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 +93 -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 +9 -6
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +168 -63
  21. data/lib/redis/connection/synchrony.rb +29 -7
  22. data/lib/redis/distributed.rb +156 -74
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +30 -73
  25. data/lib/redis/pipeline.rb +55 -15
  26. data/lib/redis/subscribe.rb +20 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +41 -170
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -59
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  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/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/command_map_test.rb +0 -30
  56. data/test/commands_on_hashes_test.rb +0 -21
  57. data/test/commands_on_hyper_log_log_test.rb +0 -21
  58. data/test/commands_on_lists_test.rb +0 -20
  59. data/test/commands_on_sets_test.rb +0 -77
  60. data/test/commands_on_sorted_sets_test.rb +0 -137
  61. data/test/commands_on_strings_test.rb +0 -101
  62. data/test/commands_on_value_types_test.rb +0 -133
  63. data/test/connection_handling_test.rb +0 -250
  64. data/test/db/.gitkeep +0 -0
  65. data/test/distributed_blocking_commands_test.rb +0 -46
  66. data/test/distributed_commands_on_hashes_test.rb +0 -10
  67. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  68. data/test/distributed_commands_on_lists_test.rb +0 -22
  69. data/test/distributed_commands_on_sets_test.rb +0 -83
  70. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  71. data/test/distributed_commands_on_strings_test.rb +0 -59
  72. data/test/distributed_commands_on_value_types_test.rb +0 -95
  73. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  74. data/test/distributed_connection_handling_test.rb +0 -23
  75. data/test/distributed_internals_test.rb +0 -79
  76. data/test/distributed_key_tags_test.rb +0 -52
  77. data/test/distributed_persistence_control_commands_test.rb +0 -26
  78. data/test/distributed_publish_subscribe_test.rb +0 -92
  79. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  80. data/test/distributed_scripting_test.rb +0 -102
  81. data/test/distributed_sorting_test.rb +0 -20
  82. data/test/distributed_test.rb +0 -58
  83. data/test/distributed_transactions_test.rb +0 -32
  84. data/test/encoding_test.rb +0 -18
  85. data/test/error_replies_test.rb +0 -59
  86. data/test/fork_safety_test.rb +0 -65
  87. data/test/helper.rb +0 -232
  88. data/test/helper_test.rb +0 -24
  89. data/test/internals_test.rb +0 -437
  90. data/test/lint/blocking_commands.rb +0 -150
  91. data/test/lint/hashes.rb +0 -162
  92. data/test/lint/hyper_log_log.rb +0 -60
  93. data/test/lint/lists.rb +0 -143
  94. data/test/lint/sets.rb +0 -125
  95. data/test/lint/sorted_sets.rb +0 -316
  96. data/test/lint/strings.rb +0 -260
  97. data/test/lint/value_types.rb +0 -122
  98. data/test/persistence_control_commands_test.rb +0 -26
  99. data/test/pipelining_commands_test.rb +0 -242
  100. data/test/publish_subscribe_test.rb +0 -254
  101. data/test/remote_server_control_commands_test.rb +0 -118
  102. data/test/scanning_test.rb +0 -413
  103. data/test/scripting_test.rb +0 -78
  104. data/test/sentinel_command_test.rb +0 -80
  105. data/test/sentinel_test.rb +0 -255
  106. data/test/sorting_test.rb +0 -59
  107. data/test/support/connection/hiredis.rb +0 -1
  108. data/test/support/connection/ruby.rb +0 -1
  109. data/test/support/connection/synchrony.rb +0 -17
  110. data/test/support/redis_mock.rb +0 -119
  111. data/test/support/wire/synchrony.rb +0 -24
  112. data/test/support/wire/thread.rb +0 -5
  113. data/test/synchrony_driver.rb +0 -88
  114. data/test/test.conf.erb +0 -9
  115. data/test/thread_safety_test.rb +0 -32
  116. data/test/transactions_test.rb +0 -264
  117. data/test/unknown_commands_test.rb +0 -14
  118. data/test/url_param_test.rb +0 -138
@@ -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
@@ -44,6 +53,7 @@ class Redis
44
53
 
45
54
  def read
46
55
  @req = EventMachine::DefaultDeferrable.new
56
+ @req.timeout(@timeout, :timeout) if @timeout > 0
47
57
  EventMachine::Synchrony.sync @req
48
58
  end
49
59
 
@@ -67,7 +77,17 @@ class Redis
67
77
 
68
78
  def self.connect(config)
69
79
  if config[:scheme] == "unix"
70
- 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"
71
91
  else
72
92
  conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c|
73
93
  c.pending_connect_timeout = [config[:connect_timeout], 0.1].max
@@ -81,7 +101,7 @@ class Redis
81
101
  raise Errno::ECONNREFUSED if Fiber.yield == :refused
82
102
 
83
103
  instance = new(conn)
84
- instance.timeout = config[:timeout]
104
+ instance.timeout = config[:read_timeout]
85
105
  instance
86
106
  end
87
107
 
@@ -90,11 +110,11 @@ class Redis
90
110
  end
91
111
 
92
112
  def connected?
93
- @connection && @connection.connected?
113
+ @connection&.connected?
94
114
  end
95
115
 
96
116
  def timeout=(timeout)
97
- @timeout = timeout
117
+ @connection.timeout = timeout
98
118
  end
99
119
 
100
120
  def disconnect
@@ -113,6 +133,8 @@ class Redis
113
133
  payload
114
134
  elsif type == :error
115
135
  raise payload
136
+ elsif type == :timeout
137
+ raise TimeoutError
116
138
  else
117
139
  raise "Unknown type #{type.inspect}"
118
140
  end
@@ -1,15 +1,17 @@
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
 
@@ -22,10 +24,14 @@ class Redis
22
24
  @default_options = options.dup
23
25
  node_configs.each { |node_config| add_node(node_config) }
24
26
  @subscribed_node = nil
27
+ @watch_key = nil
25
28
  end
26
29
 
27
30
  def node_for(key)
28
- @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)
29
35
  end
30
36
 
31
37
  def nodes
@@ -33,9 +39,9 @@ class Redis
33
39
  end
34
40
 
35
41
  def add_node(options)
36
- options = { :url => options } if options.is_a?(String)
42
+ options = { url: options } if options.is_a?(String)
37
43
  options = @default_options.merge(options)
38
- @ring.add_node Redis.new( options )
44
+ @ring.add_node Redis.new(options)
39
45
  end
40
46
 
41
47
  # Change the selected database for the current connection.
@@ -144,12 +150,12 @@ class Redis
144
150
  end
145
151
 
146
152
  # Create a key using the serialized value, previously obtained using DUMP.
147
- def restore(key, ttl, serialized_value)
148
- node_for(key).restore(key, ttl, serialized_value)
153
+ def restore(key, ttl, serialized_value, **options)
154
+ node_for(key).restore(key, ttl, serialized_value, **options)
149
155
  end
150
156
 
151
157
  # Transfer a key from the connected instance to another instance.
152
- def migrate(key, options)
158
+ def migrate(_key, _options)
153
159
  raise CannotDistribute, :migrate
154
160
  end
155
161
 
@@ -161,9 +167,42 @@ class Redis
161
167
  end
162
168
  end
163
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
+
164
178
  # Determine if a key exists.
165
- def exists(key)
166
- 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
167
206
  end
168
207
 
169
208
  # Find all keys matching the given pattern.
@@ -196,11 +235,11 @@ class Redis
196
235
  end
197
236
 
198
237
  # Sort the elements in a list, set or sorted set.
199
- def sort(key, options = {})
238
+ def sort(key, **options)
200
239
  keys = [key, options[:by], options[:store], *Array(options[:get])].compact
201
240
 
202
241
  ensure_same_node(:sort, keys) do |node|
203
- node.sort(key, options)
242
+ node.sort(key, **options)
204
243
  end
205
244
  end
206
245
 
@@ -235,8 +274,8 @@ class Redis
235
274
  end
236
275
 
237
276
  # Set the string value of a key.
238
- def set(key, value, options = {})
239
- node_for(key).set(key, value, options)
277
+ def set(key, value, **options)
278
+ node_for(key).set(key, value, **options)
240
279
  end
241
280
 
242
281
  # Set the time to live in seconds of a key.
@@ -255,20 +294,20 @@ class Redis
255
294
  end
256
295
 
257
296
  # Set multiple keys to multiple values.
258
- def mset(*args)
297
+ def mset(*_args)
259
298
  raise CannotDistribute, :mset
260
299
  end
261
300
 
262
- def mapped_mset(hash)
301
+ def mapped_mset(_hash)
263
302
  raise CannotDistribute, :mapped_mset
264
303
  end
265
304
 
266
305
  # Set multiple keys to multiple values, only if none of the keys exist.
267
- def msetnx(*args)
306
+ def msetnx(*_args)
268
307
  raise CannotDistribute, :msetnx
269
308
  end
270
309
 
271
- def mapped_msetnx(hash)
310
+ def mapped_msetnx(_hash)
272
311
  raise CannotDistribute, :mapped_msetnx
273
312
  end
274
313
 
@@ -277,13 +316,16 @@ class Redis
277
316
  node_for(key).get(key)
278
317
  end
279
318
 
280
- # Get the values of all the given keys.
319
+ # Get the values of all the given keys as an Array.
281
320
  def mget(*keys)
282
- raise CannotDistribute, :mget
321
+ mapped_mget(*keys).values_at(*keys)
283
322
  end
284
323
 
324
+ # Get the values of all the given keys as a Hash.
285
325
  def mapped_mget(*keys)
286
- 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
287
329
  end
288
330
 
289
331
  # Overwrite part of a string at key starting at the specified offset.
@@ -324,7 +366,7 @@ class Redis
324
366
  end
325
367
 
326
368
  # Return the position of the first bit set to 1 or 0 in a string.
327
- def bitpos(key, bit, start=nil, stop=nil)
369
+ def bitpos(key, bit, start = nil, stop = nil)
328
370
  node_for(key).bitpos(key, bit, start, stop)
329
371
  end
330
372
 
@@ -342,7 +384,7 @@ class Redis
342
384
  get(key)
343
385
  end
344
386
 
345
- def []=(key,value)
387
+ def []=(key, value)
346
388
  set(key, value)
347
389
  end
348
390
 
@@ -371,14 +413,14 @@ class Redis
371
413
  node_for(key).rpushx(key, value)
372
414
  end
373
415
 
374
- # Remove and get the first element in a list.
375
- def lpop(key)
376
- node_for(key).lpop(key)
416
+ # Remove and get the first elements in a list.
417
+ def lpop(key, count = nil)
418
+ node_for(key).lpop(key, count)
377
419
  end
378
420
 
379
- # Remove and get the last element in a list.
380
- def rpop(key)
381
- node_for(key).rpop(key)
421
+ # Remove and get the last elements in a list.
422
+ def rpop(key, count = nil)
423
+ node_for(key).rpop(key, count)
382
424
  end
383
425
 
384
426
  # Remove the last element in a list, append it to another list and return
@@ -390,14 +432,12 @@ class Redis
390
432
  end
391
433
 
392
434
  def _bpop(cmd, args)
393
- options = {}
394
-
395
- case args.last
396
- when Hash
435
+ timeout = if args.last.is_a?(Hash)
397
436
  options = args.pop
398
- when Integer
437
+ options[:timeout]
438
+ elsif args.last.respond_to?(:to_int)
399
439
  # Issue deprecation notice in obnoxious mode...
400
- options[:timeout] = args.pop
440
+ args.pop.to_int
401
441
  end
402
442
 
403
443
  if args.size > 1
@@ -407,7 +447,11 @@ class Redis
407
447
  keys = args.flatten
408
448
 
409
449
  ensure_same_node(cmd, keys) do |node|
410
- node.__send__(cmd, keys, options)
450
+ if timeout
451
+ node.__send__(cmd, keys, timeout: timeout)
452
+ else
453
+ node.__send__(cmd, keys)
454
+ end
411
455
  end
412
456
  end
413
457
 
@@ -425,15 +469,9 @@ class Redis
425
469
 
426
470
  # Pop a value from a list, push it to another list and return it; or block
427
471
  # until one is available.
428
- def brpoplpush(source, destination, options = {})
429
- case options
430
- when Integer
431
- # Issue deprecation notice in obnoxious mode...
432
- options = { :timeout => options }
433
- end
434
-
472
+ def brpoplpush(source, destination, deprecated_timeout = 0, **options)
435
473
  ensure_same_node(:brpoplpush, [source, destination]) do |node|
436
- node.brpoplpush(source, destination, options)
474
+ node.brpoplpush(source, destination, deprecated_timeout, **options)
437
475
  end
438
476
  end
439
477
 
@@ -483,8 +521,8 @@ class Redis
483
521
  end
484
522
 
485
523
  # Remove and return a random member from a set.
486
- def spop(key)
487
- node_for(key).spop(key)
524
+ def spop(key, count = nil)
525
+ node_for(key).spop(key, count)
488
526
  end
489
527
 
490
528
  # Get a random member from a set.
@@ -509,6 +547,16 @@ class Redis
509
547
  node_for(key).smembers(key)
510
548
  end
511
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
+
512
560
  # Subtract multiple sets.
513
561
  def sdiff(*keys)
514
562
  ensure_same_node(:sdiff, keys) do |node|
@@ -561,6 +609,7 @@ class Redis
561
609
  def zadd(key, *args)
562
610
  node_for(key).zadd(key, *args)
563
611
  end
612
+ ruby2_keywords(:zadd) if respond_to?(:ruby2_keywords, true)
564
613
 
565
614
  # Increment the score of a member in a sorted set.
566
615
  def zincrby(key, increment, member)
@@ -578,14 +627,14 @@ class Redis
578
627
  end
579
628
 
580
629
  # Return a range of members in a sorted set, by index.
581
- def zrange(key, start, stop, options = {})
582
- node_for(key).zrange(key, start, stop, options)
630
+ def zrange(key, start, stop, **options)
631
+ node_for(key).zrange(key, start, stop, **options)
583
632
  end
584
633
 
585
634
  # Return a range of members in a sorted set, by index, with scores ordered
586
635
  # from high to low.
587
- def zrevrange(key, start, stop, options = {})
588
- node_for(key).zrevrange(key, start, stop, options)
636
+ def zrevrange(key, start, stop, **options)
637
+ node_for(key).zrevrange(key, start, stop, **options)
589
638
  end
590
639
 
591
640
  # Determine the index of a member in a sorted set.
@@ -605,14 +654,14 @@ class Redis
605
654
  end
606
655
 
607
656
  # Return a range of members in a sorted set, by score.
608
- def zrangebyscore(key, min, max, options = {})
609
- node_for(key).zrangebyscore(key, min, max, options)
657
+ def zrangebyscore(key, min, max, **options)
658
+ node_for(key).zrangebyscore(key, min, max, **options)
610
659
  end
611
660
 
612
661
  # Return a range of members in a sorted set, by score, with scores ordered
613
662
  # from high to low.
614
- def zrevrangebyscore(key, max, min, options = {})
615
- node_for(key).zrevrangebyscore(key, max, min, options)
663
+ def zrevrangebyscore(key, max, min, **options)
664
+ node_for(key).zrevrangebyscore(key, max, min, **options)
616
665
  end
617
666
 
618
667
  # Remove all members in a sorted set within the given scores.
@@ -625,18 +674,25 @@ class Redis
625
674
  node_for(key).zcount(key, min, max)
626
675
  end
627
676
 
677
+ # Get the intersection of multiple sorted sets
678
+ def zinter(*keys, **options)
679
+ ensure_same_node(:zinter, keys) do |node|
680
+ node.zinter(*keys, **options)
681
+ end
682
+ end
683
+
628
684
  # Intersect multiple sorted sets and store the resulting sorted set in a new
629
685
  # key.
630
- def zinterstore(destination, keys, options = {})
686
+ def zinterstore(destination, keys, **options)
631
687
  ensure_same_node(:zinterstore, [destination] + keys) do |node|
632
- node.zinterstore(destination, keys, options)
688
+ node.zinterstore(destination, keys, **options)
633
689
  end
634
690
  end
635
691
 
636
692
  # Add multiple sorted sets and store the resulting sorted set in a new key.
637
- def zunionstore(destination, keys, options = {})
693
+ def zunionstore(destination, keys, **options)
638
694
  ensure_same_node(:zunionstore, [destination] + keys) do |node|
639
- node.zunionstore(destination, keys, options)
695
+ node.zunionstore(destination, keys, **options)
640
696
  end
641
697
  end
642
698
 
@@ -645,9 +701,9 @@ class Redis
645
701
  node_for(key).hlen(key)
646
702
  end
647
703
 
648
- # Set the string value of a hash field.
649
- def hset(key, field, value)
650
- node_for(key).hset(key, field, value)
704
+ # Set multiple hash fields to multiple values.
705
+ def hset(key, *attrs)
706
+ node_for(key).hset(key, *attrs)
651
707
  end
652
708
 
653
709
  # Set the value of a hash field, only if the field does not exist.
@@ -679,8 +735,8 @@ class Redis
679
735
  end
680
736
 
681
737
  # Delete one or more hash fields.
682
- def hdel(key, field)
683
- node_for(key).hdel(key, field)
738
+ def hdel(key, *fields)
739
+ node_for(key).hdel(key, *fields)
684
740
  end
685
741
 
686
742
  # Determine if a hash field exists.
@@ -719,7 +775,7 @@ class Redis
719
775
  end
720
776
 
721
777
  def subscribed?
722
- !! @subscribed_node
778
+ !!@subscribed_node
723
779
  end
724
780
 
725
781
  # Listen for messages published to the given channels.
@@ -737,7 +793,8 @@ class Redis
737
793
 
738
794
  # Stop listening for messages posted to the given channels.
739
795
  def unsubscribe(*channels)
740
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
796
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
797
+
741
798
  @subscribed_node.unsubscribe(*channels)
742
799
  end
743
800
 
@@ -753,13 +810,26 @@ class Redis
753
810
  end
754
811
 
755
812
  # Watch the given keys to determine execution of the MULTI/EXEC block.
756
- def watch(*keys)
757
- raise CannotDistribute, :watch
813
+ def watch(*keys, &block)
814
+ ensure_same_node(:watch, keys) do |node|
815
+ @watch_key = key_tag(keys.first) || keys.first.to_s
816
+
817
+ begin
818
+ node.watch(*keys, &block)
819
+ rescue StandardError
820
+ @watch_key = nil
821
+ raise
822
+ end
823
+ end
758
824
  end
759
825
 
760
826
  # Forget about all watched keys.
761
827
  def unwatch
762
- raise CannotDistribute, :unwatch
828
+ raise CannotDistribute, :unwatch unless @watch_key
829
+
830
+ result = node_for(@watch_key).unwatch
831
+ @watch_key = nil
832
+ result
763
833
  end
764
834
 
765
835
  def pipelined
@@ -767,18 +837,30 @@ class Redis
767
837
  end
768
838
 
769
839
  # Mark the start of a transaction block.
770
- def multi
771
- raise CannotDistribute, :multi
840
+ def multi(&block)
841
+ raise CannotDistribute, :multi unless @watch_key
842
+
843
+ result = node_for(@watch_key).multi(&block)
844
+ @watch_key = nil if block_given?
845
+ result
772
846
  end
773
847
 
774
848
  # Execute all commands issued after MULTI.
775
849
  def exec
776
- raise CannotDistribute, :exec
850
+ raise CannotDistribute, :exec unless @watch_key
851
+
852
+ result = node_for(@watch_key).exec
853
+ @watch_key = nil
854
+ result
777
855
  end
778
856
 
779
857
  # Discard all commands issued after MULTI.
780
858
  def discard
781
- raise CannotDistribute, :discard
859
+ raise CannotDistribute, :discard unless @watch_key
860
+
861
+ result = node_for(@watch_key).discard
862
+ @watch_key = nil
863
+ result
782
864
  end
783
865
 
784
866
  # Control remote script registry.
@@ -837,7 +919,7 @@ class Redis
837
919
  self.class.new(@node_configs, @default_options)
838
920
  end
839
921
 
840
- protected
922
+ protected
841
923
 
842
924
  def on_each_node(command, *args)
843
925
  nodes.map do |node|