redis 3.3.5 → 4.3.1

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 (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +132 -2
  3. data/README.md +144 -79
  4. data/lib/redis.rb +1174 -405
  5. data/lib/redis/client.rb +150 -90
  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 +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 +6 -5
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +126 -128
  21. data/lib/redis/connection/synchrony.rb +21 -8
  22. data/lib/redis/distributed.rb +147 -72
  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 +11 -12
  27. data/lib/redis/version.rb +3 -1
  28. metadata +49 -202
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -89
  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/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
@@ -1,9 +1,16 @@
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
@@ -46,9 +53,7 @@ class Redis
46
53
 
47
54
  def read
48
55
  @req = EventMachine::DefaultDeferrable.new
49
- if @timeout > 0
50
- @req.timeout(@timeout, :timeout)
51
- end
56
+ @req.timeout(@timeout, :timeout) if @timeout > 0
52
57
  EventMachine::Synchrony.sync @req
53
58
  end
54
59
 
@@ -72,7 +77,15 @@ class Redis
72
77
 
73
78
  def self.connect(config)
74
79
  if config[:scheme] == "unix"
75
- 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
76
89
  elsif config[:scheme] == "rediss" || config[:ssl]
77
90
  raise NotImplementedError, "SSL not supported by synchrony driver"
78
91
  else
@@ -97,7 +110,7 @@ class Redis
97
110
  end
98
111
 
99
112
  def connected?
100
- @connection && @connection.connected?
113
+ @connection&.connected?
101
114
  end
102
115
 
103
116
  def timeout=(timeout)
@@ -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
 
@@ -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.
@@ -627,16 +676,16 @@ class Redis
627
676
 
628
677
  # Intersect multiple sorted sets and store the resulting sorted set in a new
629
678
  # key.
630
- def zinterstore(destination, keys, options = {})
679
+ def zinterstore(destination, keys, **options)
631
680
  ensure_same_node(:zinterstore, [destination] + keys) do |node|
632
- node.zinterstore(destination, keys, options)
681
+ node.zinterstore(destination, keys, **options)
633
682
  end
634
683
  end
635
684
 
636
685
  # Add multiple sorted sets and store the resulting sorted set in a new key.
637
- def zunionstore(destination, keys, options = {})
686
+ def zunionstore(destination, keys, **options)
638
687
  ensure_same_node(:zunionstore, [destination] + keys) do |node|
639
- node.zunionstore(destination, keys, options)
688
+ node.zunionstore(destination, keys, **options)
640
689
  end
641
690
  end
642
691
 
@@ -645,9 +694,9 @@ class Redis
645
694
  node_for(key).hlen(key)
646
695
  end
647
696
 
648
- # Set the string value of a hash field.
649
- def hset(key, field, value)
650
- 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)
651
700
  end
652
701
 
653
702
  # Set the value of a hash field, only if the field does not exist.
@@ -679,8 +728,8 @@ class Redis
679
728
  end
680
729
 
681
730
  # Delete one or more hash fields.
682
- def hdel(key, field)
683
- node_for(key).hdel(key, field)
731
+ def hdel(key, *fields)
732
+ node_for(key).hdel(key, *fields)
684
733
  end
685
734
 
686
735
  # Determine if a hash field exists.
@@ -719,7 +768,7 @@ class Redis
719
768
  end
720
769
 
721
770
  def subscribed?
722
- !! @subscribed_node
771
+ !!@subscribed_node
723
772
  end
724
773
 
725
774
  # Listen for messages published to the given channels.
@@ -737,7 +786,8 @@ class Redis
737
786
 
738
787
  # Stop listening for messages posted to the given channels.
739
788
  def unsubscribe(*channels)
740
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
789
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
790
+
741
791
  @subscribed_node.unsubscribe(*channels)
742
792
  end
743
793
 
@@ -753,13 +803,26 @@ class Redis
753
803
  end
754
804
 
755
805
  # Watch the given keys to determine execution of the MULTI/EXEC block.
756
- def watch(*keys)
757
- 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
758
817
  end
759
818
 
760
819
  # Forget about all watched keys.
761
820
  def unwatch
762
- raise CannotDistribute, :unwatch
821
+ raise CannotDistribute, :unwatch unless @watch_key
822
+
823
+ result = node_for(@watch_key).unwatch
824
+ @watch_key = nil
825
+ result
763
826
  end
764
827
 
765
828
  def pipelined
@@ -767,18 +830,30 @@ class Redis
767
830
  end
768
831
 
769
832
  # Mark the start of a transaction block.
770
- def multi
771
- 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
772
839
  end
773
840
 
774
841
  # Execute all commands issued after MULTI.
775
842
  def exec
776
- raise CannotDistribute, :exec
843
+ raise CannotDistribute, :exec unless @watch_key
844
+
845
+ result = node_for(@watch_key).exec
846
+ @watch_key = nil
847
+ result
777
848
  end
778
849
 
779
850
  # Discard all commands issued after MULTI.
780
851
  def discard
781
- raise CannotDistribute, :discard
852
+ raise CannotDistribute, :discard unless @watch_key
853
+
854
+ result = node_for(@watch_key).discard
855
+ @watch_key = nil
856
+ result
782
857
  end
783
858
 
784
859
  # Control remote script registry.
@@ -837,7 +912,7 @@ class Redis
837
912
  self.class.new(@node_configs, @default_options)
838
913
  end
839
914
 
840
- protected
915
+ protected
841
916
 
842
917
  def on_each_node(command, *args)
843
918
  nodes.map do |node|