redis 3.0.0.rc1 → 3.0.0.rc2

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 (77) hide show
  1. data/.travis.yml +50 -0
  2. data/.travis/Gemfile +11 -0
  3. data/CHANGELOG.md +47 -19
  4. data/README.md +160 -149
  5. data/Rakefile +15 -50
  6. data/examples/pubsub.rb +1 -1
  7. data/examples/unicorn/config.ru +1 -1
  8. data/examples/unicorn/unicorn.rb +1 -1
  9. data/lib/redis.rb +790 -390
  10. data/lib/redis/client.rb +137 -49
  11. data/lib/redis/connection/hiredis.rb +26 -15
  12. data/lib/redis/connection/ruby.rb +170 -53
  13. data/lib/redis/connection/synchrony.rb +23 -35
  14. data/lib/redis/distributed.rb +92 -32
  15. data/lib/redis/errors.rb +4 -2
  16. data/lib/redis/pipeline.rb +17 -6
  17. data/lib/redis/version.rb +1 -1
  18. data/redis.gemspec +4 -6
  19. data/test/blocking_commands_test.rb +42 -0
  20. data/test/command_map_test.rb +18 -17
  21. data/test/commands_on_hashes_test.rb +13 -12
  22. data/test/commands_on_lists_test.rb +35 -45
  23. data/test/commands_on_sets_test.rb +55 -54
  24. data/test/commands_on_sorted_sets_test.rb +106 -105
  25. data/test/commands_on_strings_test.rb +64 -55
  26. data/test/commands_on_value_types_test.rb +66 -54
  27. data/test/connection_handling_test.rb +136 -151
  28. data/test/distributed_blocking_commands_test.rb +33 -40
  29. data/test/distributed_commands_on_hashes_test.rb +6 -7
  30. data/test/distributed_commands_on_lists_test.rb +13 -14
  31. data/test/distributed_commands_on_sets_test.rb +57 -58
  32. data/test/distributed_commands_on_sorted_sets_test.rb +11 -12
  33. data/test/distributed_commands_on_strings_test.rb +31 -32
  34. data/test/distributed_commands_on_value_types_test.rb +61 -46
  35. data/test/distributed_commands_requiring_clustering_test.rb +108 -108
  36. data/test/distributed_connection_handling_test.rb +14 -15
  37. data/test/distributed_internals_test.rb +7 -19
  38. data/test/distributed_key_tags_test.rb +36 -36
  39. data/test/distributed_persistence_control_commands_test.rb +17 -14
  40. data/test/distributed_publish_subscribe_test.rb +61 -69
  41. data/test/distributed_remote_server_control_commands_test.rb +39 -28
  42. data/test/distributed_sorting_test.rb +12 -13
  43. data/test/distributed_test.rb +40 -41
  44. data/test/distributed_transactions_test.rb +20 -21
  45. data/test/encoding_test.rb +12 -9
  46. data/test/error_replies_test.rb +42 -36
  47. data/test/helper.rb +118 -85
  48. data/test/helper_test.rb +20 -6
  49. data/test/internals_test.rb +167 -103
  50. data/test/lint/blocking_commands.rb +124 -0
  51. data/test/lint/hashes.rb +115 -93
  52. data/test/lint/lists.rb +86 -80
  53. data/test/lint/sets.rb +68 -62
  54. data/test/lint/sorted_sets.rb +200 -195
  55. data/test/lint/strings.rb +112 -94
  56. data/test/lint/value_types.rb +76 -55
  57. data/test/persistence_control_commands_test.rb +17 -12
  58. data/test/pipelining_commands_test.rb +135 -126
  59. data/test/publish_subscribe_test.rb +105 -110
  60. data/test/remote_server_control_commands_test.rb +74 -58
  61. data/test/sorting_test.rb +31 -29
  62. data/test/support/connection/hiredis.rb +1 -0
  63. data/test/support/connection/ruby.rb +1 -0
  64. data/test/support/connection/synchrony.rb +17 -0
  65. data/test/{redis_mock.rb → support/redis_mock.rb} +24 -21
  66. data/test/support/wire/synchrony.rb +24 -0
  67. data/test/support/wire/thread.rb +5 -0
  68. data/test/synchrony_driver.rb +9 -9
  69. data/test/test.conf +1 -1
  70. data/test/thread_safety_test.rb +21 -19
  71. data/test/transactions_test.rb +189 -118
  72. data/test/unknown_commands_test.rb +9 -8
  73. data/test/url_param_test.rb +46 -41
  74. metadata +28 -43
  75. data/TODO.md +0 -4
  76. data/benchmarking/thread_safety.rb +0 -38
  77. data/test/lint/internals.rb +0 -36
@@ -60,30 +60,36 @@ class Redis
60
60
  class Synchrony
61
61
  include Redis::Connection::CommandHelper
62
62
 
63
- def initialize
64
- @timeout = 5_000_000
65
- @connection = nil
66
- end
63
+ def self.connect(config)
64
+ if config[:scheme] == "unix"
65
+ conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
66
+ else
67
+ conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c|
68
+ c.pending_connect_timeout = [config[:timeout], 0.1].max
69
+ end
70
+ end
67
71
 
68
- def connected?
69
- @connection && @connection.connected?
70
- end
72
+ fiber = Fiber.current
73
+ conn.callback { fiber.resume }
74
+ conn.errback { fiber.resume :refused }
71
75
 
72
- def timeout=(usecs)
73
- @timeout = usecs
76
+ raise Errno::ECONNREFUSED if Fiber.yield == :refused
77
+
78
+ instance = new(conn)
79
+ instance.timeout = config[:timeout]
80
+ instance
74
81
  end
75
82
 
76
- def connect(host, port, timeout)
77
- conn = EventMachine.connect(host, port, RedisClient) do |c|
78
- c.pending_connect_timeout = [Float(timeout / 1_000_000), 0.1].max
79
- end
83
+ def initialize(connection)
84
+ @connection = connection
85
+ end
80
86
 
81
- setup_connect_callbacks(conn, Fiber.current)
87
+ def connected?
88
+ @connection && @connection.connected?
82
89
  end
83
90
 
84
- def connect_unix(path, timeout)
85
- conn = EventMachine.connect_unix_domain(path, RedisClient)
86
- setup_connect_callbacks(conn, Fiber.current)
91
+ def timeout=(timeout)
92
+ @timeout = timeout
87
93
  end
88
94
 
89
95
  def disconnect
@@ -106,24 +112,6 @@ class Redis
106
112
  raise "Unknown type #{type.inspect}"
107
113
  end
108
114
  end
109
-
110
- private
111
-
112
- def setup_connect_callbacks(conn, f)
113
- conn.callback do
114
- @connection = conn
115
- f.resume conn
116
- end
117
-
118
- conn.errback do
119
- @connection = conn
120
- f.resume :refused
121
- end
122
-
123
- r = Fiber.yield
124
- raise Errno::ECONNREFUSED if r == :refused
125
- r
126
- end
127
115
  end
128
116
  end
129
117
  end
@@ -18,7 +18,7 @@ class Redis
18
18
  def initialize(urls, options = {})
19
19
  @tag = options.delete(:tag) || /^\{(.+?)\}/
20
20
  @default_options = options
21
- @ring = HashRing.new urls.map { |url| Redis.connect(options.merge(:url => url)) }
21
+ @ring = HashRing.new urls.map { |url| Redis.new(options.merge(:url => url)) }
22
22
  @subscribed_node = nil
23
23
  end
24
24
 
@@ -31,7 +31,7 @@ class Redis
31
31
  end
32
32
 
33
33
  def add_node(url)
34
- @ring.add_node Redis.connect(@default_options.merge(:url => url))
34
+ @ring.add_node Redis.new(@default_options.merge(:url => url))
35
35
  end
36
36
 
37
37
  # Close the connection.
@@ -84,14 +84,14 @@ class Redis
84
84
 
85
85
  # Rename a key.
86
86
  def rename(old_name, new_name)
87
- ensure_same_node(:rename, old_name, new_name) do |node|
87
+ ensure_same_node(:rename, [old_name, new_name]) do |node|
88
88
  node.rename(old_name, new_name)
89
89
  end
90
90
  end
91
91
 
92
92
  # Rename a key, only if the new key does not exist.
93
93
  def renamenx(old_name, new_name)
94
- ensure_same_node(:renamenx, old_name, new_name) do |node|
94
+ ensure_same_node(:renamenx, [old_name, new_name]) do |node|
95
95
  node.renamenx(old_name, new_name)
96
96
  end
97
97
  end
@@ -106,21 +106,36 @@ class Redis
106
106
  node_for(key).expire(key, seconds)
107
107
  end
108
108
 
109
+ # Set a key's time to live in milliseconds.
110
+ def pexpire(key, milliseconds)
111
+ node_for(key).pexpire(key, milliseconds)
112
+ end
113
+
109
114
  # Set the expiration for a key as a UNIX timestamp.
110
115
  def expireat(key, unix_time)
111
116
  node_for(key).expireat(key, unix_time)
112
117
  end
113
118
 
119
+ # Set the expiration for a key as number of milliseconds from UNIX Epoch.
120
+ def pexpireat(key, ms_unix_time)
121
+ node_for(key).pexpireat(key, ms_unix_time)
122
+ end
123
+
114
124
  # Remove the expiration from a key.
115
125
  def persist(key)
116
126
  node_for(key).persist(key)
117
127
  end
118
128
 
119
- # Get the time to live for a key.
129
+ # Get the time to live (in seconds) for a key.
120
130
  def ttl(key)
121
131
  node_for(key).ttl(key)
122
132
  end
123
133
 
134
+ # Get the time to live (in milliseconds) for a key.
135
+ def pttl(key)
136
+ node_for(key).pttl(key)
137
+ end
138
+
124
139
  # Move a key to another database.
125
140
  def move(key, db)
126
141
  node_for(key).move(key, db)
@@ -146,11 +161,16 @@ class Redis
146
161
  node_for(key).setrange(key, offset, value)
147
162
  end
148
163
 
149
- # Set the value and expiration of a key.
164
+ # Set the time to live in seconds of a key.
150
165
  def setex(key, ttl, value)
151
166
  node_for(key).setex(key, ttl, value)
152
167
  end
153
168
 
169
+ # Set the time to live in milliseconds of a key.
170
+ def psetex(key, ttl, value)
171
+ node_for(key).psetex(key, ttl, value)
172
+ end
173
+
154
174
  # Get the value of a key.
155
175
  def get(key)
156
176
  node_for(key).get(key)
@@ -221,11 +241,16 @@ class Redis
221
241
  node_for(key).incr(key)
222
242
  end
223
243
 
224
- # Increment the integer value of a key by the given number.
244
+ # Increment the integer value of a key by the given integer number.
225
245
  def incrby(key, increment)
226
246
  node_for(key).incrby(key, increment)
227
247
  end
228
248
 
249
+ # Increment the numeric value of a key by the given float number.
250
+ def incrbyfloat(key, increment)
251
+ node_for(key).incrbyfloat(key, increment)
252
+ end
253
+
229
254
  # Decrement the integer value of a key by one.
230
255
  def decr(key)
231
256
  node_for(key).decr(key)
@@ -289,28 +314,56 @@ class Redis
289
314
  # Remove the last element in a list, append it to another list and return
290
315
  # it.
291
316
  def rpoplpush(source, destination)
292
- ensure_same_node(:rpoplpush, source, destination) do |node|
317
+ ensure_same_node(:rpoplpush, [source, destination]) do |node|
293
318
  node.rpoplpush(source, destination)
294
319
  end
295
320
  end
296
321
 
322
+ def _bpop(cmd, args)
323
+ options = {}
324
+
325
+ case args.last
326
+ when Hash
327
+ options = args.pop
328
+ when Integer
329
+ # Issue deprecation notice in obnoxious mode...
330
+ options[:timeout] = args.pop
331
+ end
332
+
333
+ if args.size > 1
334
+ # Issue deprecation notice in obnoxious mode...
335
+ end
336
+
337
+ keys = args.flatten
338
+
339
+ ensure_same_node(cmd, keys) do |node|
340
+ node.__send__(cmd, keys, options)
341
+ end
342
+ end
343
+
297
344
  # Remove and get the first element in a list, or block until one is
298
345
  # available.
299
- def blpop(key, timeout)
300
- node_for(key).blpop(key, timeout)
346
+ def blpop(*args)
347
+ _bpop(:blpop, args)
301
348
  end
302
349
 
303
350
  # Remove and get the last element in a list, or block until one is
304
351
  # available.
305
- def brpop(key, timeout)
306
- node_for(key).brpop(key, timeout)
352
+ def brpop(*args)
353
+ _bpop(:brpop, args)
307
354
  end
308
355
 
309
356
  # Pop a value from a list, push it to another list and return it; or block
310
357
  # until one is available.
311
- def brpoplpush(source, destination, timeout)
312
- ensure_same_node(:brpoplpush, source, destination) do |node|
313
- node.brpoplpush(source, destination, timeout)
358
+ def brpoplpush(source, destination, options = {})
359
+ case options
360
+ when Integer
361
+ # Issue deprecation notice in obnoxious mode...
362
+ options = { :timeout => options }
363
+ end
364
+
365
+ ensure_same_node(:brpoplpush, [source, destination]) do |node|
366
+ node.brpoplpush(source, destination, options)
314
367
  end
315
368
  end
316
369
 
@@ -331,7 +384,7 @@ class Redis
331
384
 
332
385
  # Move a member from one set to another.
333
386
  def smove(source, destination, member)
334
- ensure_same_node(:smove, source, destination) do |node|
387
+ ensure_same_node(:smove, [source, destination]) do |node|
335
388
  node.smove(source, destination, member)
336
389
  end
337
390
  end
@@ -348,42 +401,42 @@ class Redis
348
401
 
349
402
  # Intersect multiple sets.
350
403
  def sinter(*keys)
351
- ensure_same_node(:sinter, *keys) do |node|
404
+ ensure_same_node(:sinter, keys) do |node|
352
405
  node.sinter(*keys)
353
406
  end
354
407
  end
355
408
 
356
409
  # Intersect multiple sets and store the resulting set in a key.
357
410
  def sinterstore(destination, *keys)
358
- ensure_same_node(:sinterstore, destination, *keys) do |node|
411
+ ensure_same_node(:sinterstore, [destination] + keys) do |node|
359
412
  node.sinterstore(destination, *keys)
360
413
  end
361
414
  end
362
415
 
363
416
  # Add multiple sets.
364
417
  def sunion(*keys)
365
- ensure_same_node(:sunion, *keys) do |node|
418
+ ensure_same_node(:sunion, keys) do |node|
366
419
  node.sunion(*keys)
367
420
  end
368
421
  end
369
422
 
370
423
  # Add multiple sets and store the resulting set in a key.
371
424
  def sunionstore(destination, *keys)
372
- ensure_same_node(:sunionstore, destination, *keys) do |node|
425
+ ensure_same_node(:sunionstore, [destination] + keys) do |node|
373
426
  node.sunionstore(destination, *keys)
374
427
  end
375
428
  end
376
429
 
377
430
  # Subtract multiple sets.
378
431
  def sdiff(*keys)
379
- ensure_same_node(:sdiff, *keys) do |node|
432
+ ensure_same_node(:sdiff, keys) do |node|
380
433
  node.sdiff(*keys)
381
434
  end
382
435
  end
383
436
 
384
437
  # Subtract multiple sets and store the resulting set in a key.
385
438
  def sdiffstore(destination, *keys)
386
- ensure_same_node(:sdiffstore, destination, *keys) do |node|
439
+ ensure_same_node(:sdiffstore, [destination] + keys) do |node|
387
440
  node.sdiffstore(destination, *keys)
388
441
  end
389
442
  end
@@ -475,14 +528,14 @@ class Redis
475
528
  # Intersect multiple sorted sets and store the resulting sorted set in a new
476
529
  # key.
477
530
  def zinterstore(destination, keys, options = {})
478
- ensure_same_node(:zinterstore, destination, *keys) do |node|
531
+ ensure_same_node(:zinterstore, [destination] + keys) do |node|
479
532
  node.zinterstore(destination, keys, options)
480
533
  end
481
534
  end
482
535
 
483
536
  # Add multiple sorted sets and store the resulting sorted set in a new key.
484
537
  def zunionstore(destination, keys, options = {})
485
- ensure_same_node(:zunionstore, destination, *keys) do |node|
538
+ ensure_same_node(:zunionstore, [destination] + keys) do |node|
486
539
  node.zunionstore(destination, keys, options)
487
540
  end
488
541
  end
@@ -550,16 +603,21 @@ class Redis
550
603
  Hash[*fields.zip(hmget(key, *fields)).flatten]
551
604
  end
552
605
 
553
- # Increment the integer value of a hash field by the given number.
606
+ # Increment the integer value of a hash field by the given integer number.
554
607
  def hincrby(key, field, increment)
555
608
  node_for(key).hincrby(key, field, increment)
556
609
  end
557
610
 
611
+ # Increment the numeric value of a hash field by the given float number.
612
+ def hincrbyfloat(key, field, increment)
613
+ node_for(key).hincrbyfloat(key, field, increment)
614
+ end
615
+
558
616
  # Sort the elements in a list, set or sorted set.
559
617
  def sort(key, options = {})
560
618
  keys = [key, options[:by], options[:store], *Array(options[:get])].compact
561
619
 
562
- ensure_same_node(:sort, *keys) do |node|
620
+ ensure_same_node(:sort, keys) do |node|
563
621
  node.sort(key, options)
564
622
  end
565
623
  end
@@ -610,7 +668,7 @@ class Redis
610
668
  @subscribed_node = node_for(channel)
611
669
  @subscribed_node.subscribe(channel, &block)
612
670
  else
613
- ensure_same_node(:subscribe, channel, *channels) do |node|
671
+ ensure_same_node(:subscribe, [channel] + channels) do |node|
614
672
  @subscribed_node = node
615
673
  node.subscribe(channel, *channels, &block)
616
674
  end
@@ -658,15 +716,17 @@ class Redis
658
716
  on_each_node :echo, value
659
717
  end
660
718
 
719
+ # Get server time: an UNIX timestamp and the elapsed microseconds in the current second.
720
+ def time
721
+ on_each_node :time
722
+ end
723
+
661
724
  def pipelined
662
725
  raise CannotDistribute, :pipelined
663
726
  end
664
727
 
665
728
  def inspect
666
- node_info = nodes.map do |node|
667
- "#{node.id} (Redis v#{node.info['redis_version']})"
668
- end
669
- "#<Redis client v#{Redis::VERSION} connected to #{node_info.join(', ')}>"
729
+ "#<Redis client v#{Redis::VERSION} for #{nodes.map(&:id).join(', ')}>"
670
730
  end
671
731
 
672
732
  protected
@@ -685,7 +745,7 @@ class Redis
685
745
  key.to_s[@tag, 1] if @tag
686
746
  end
687
747
 
688
- def ensure_same_node(command, *keys)
748
+ def ensure_same_node(command, keys)
689
749
  tags = keys.map { |key| key_tag(key) }
690
750
 
691
751
  raise CannotDistribute, command if !tags.all? || tags.uniq.size != 1
@@ -8,8 +8,6 @@ class Redis
8
8
  def initialize(reply_type)
9
9
  super(<<-EOS.gsub(/(?:^|\n)\s*/, " "))
10
10
  Got '#{reply_type}' as initial reply byte.
11
- If you're running in a multi-threaded environment, make sure you
12
- pass the :thread_safe option when initializing the connection.
13
11
  If you're in a forking environment, such as Unicorn, you need to
14
12
  connect to Redis after forking.
15
13
  EOS
@@ -35,4 +33,8 @@ class Redis
35
33
  # Raised when performing I/O times out.
36
34
  class TimeoutError < BaseConnectionError
37
35
  end
36
+
37
+ # Raised when the connection was inherited by a child process.
38
+ class InheritedError < BaseConnectionError
39
+ end
38
40
  end
@@ -46,9 +46,15 @@ class Redis
46
46
  yield
47
47
  end
48
48
 
49
- def finish(replies)
50
- futures.each_with_index.map do |future, i|
51
- future._set(replies[i])
49
+ def finish(replies, &blk)
50
+ if blk
51
+ futures.each_with_index.map do |future, i|
52
+ future._set(blk.call(replies[i]))
53
+ end
54
+ else
55
+ futures.each_with_index.map do |future, i|
56
+ future._set(replies[i])
57
+ end
52
58
  end
53
59
  end
54
60
 
@@ -58,10 +64,15 @@ class Redis
58
64
 
59
65
  if replies.last.size < futures.size - 2
60
66
  # Some command wasn't recognized by Redis.
61
- raise replies.detect { |r| r.kind_of?(::Exception) }
67
+ raise replies.detect { |r| r.kind_of?(::RuntimeError) }
62
68
  end
63
69
 
64
- super(replies.last)
70
+ super(replies.last) do |reply|
71
+ # Because an EXEC returns nested replies, hiredis won't be able to
72
+ # convert an error reply to a CommandError instance itself. This is
73
+ # specific to MULTI/EXEC, so we solve this here.
74
+ reply.is_a?(::RuntimeError) ? CommandError.new(reply.message) : reply
75
+ end
65
76
  end
66
77
 
67
78
  def commands
@@ -99,7 +110,7 @@ class Redis
99
110
  end
100
111
 
101
112
  def value
102
- ::Kernel.raise(@object) if @object.kind_of?(::Exception)
113
+ ::Kernel.raise(@object) if @object.kind_of?(::RuntimeError)
103
114
  @object
104
115
  end
105
116
  end
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "3.0.0.rc1"
2
+ VERSION = "3.0.0.rc2"
3
3
  end