redis 2.2.2 → 3.0.0.rc1

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 (61) hide show
  1. data/.gitignore +2 -0
  2. data/.yardopts +3 -0
  3. data/CHANGELOG.md +65 -1
  4. data/README.md +6 -0
  5. data/Rakefile +19 -27
  6. data/lib/redis.rb +737 -170
  7. data/lib/redis/client.rb +82 -67
  8. data/lib/redis/connection/command_helper.rb +15 -16
  9. data/lib/redis/connection/hiredis.rb +6 -3
  10. data/lib/redis/connection/ruby.rb +2 -1
  11. data/lib/redis/connection/synchrony.rb +3 -1
  12. data/lib/redis/distributed.rb +20 -18
  13. data/lib/redis/errors.rb +38 -0
  14. data/lib/redis/hash_ring.rb +2 -2
  15. data/lib/redis/pipeline.rb +91 -19
  16. data/lib/redis/subscribe.rb +1 -16
  17. data/lib/redis/version.rb +1 -1
  18. data/redis.gemspec +30 -11
  19. data/test/command_map_test.rb +29 -0
  20. data/test/commands_on_hashes_test.rb +3 -3
  21. data/test/commands_on_lists_test.rb +1 -1
  22. data/test/commands_on_sets_test.rb +0 -2
  23. data/test/commands_on_sorted_sets_test.rb +8 -9
  24. data/test/commands_on_strings_test.rb +3 -3
  25. data/test/commands_on_value_types_test.rb +0 -1
  26. data/test/connection_handling_test.rb +120 -4
  27. data/test/distributed_commands_on_hashes_test.rb +0 -1
  28. data/test/distributed_commands_on_lists_test.rb +0 -1
  29. data/test/distributed_commands_on_sets_test.rb +0 -1
  30. data/test/distributed_commands_on_sorted_sets_test.rb +19 -0
  31. data/test/distributed_commands_on_strings_test.rb +0 -1
  32. data/test/distributed_commands_on_value_types_test.rb +0 -1
  33. data/test/distributed_connection_handling_test.rb +0 -1
  34. data/test/distributed_key_tags_test.rb +0 -1
  35. data/test/distributed_persistence_control_commands_test.rb +0 -1
  36. data/test/distributed_publish_subscribe_test.rb +1 -2
  37. data/test/distributed_remote_server_control_commands_test.rb +2 -3
  38. data/test/distributed_transactions_test.rb +0 -1
  39. data/test/encoding_test.rb +0 -1
  40. data/test/helper.rb +14 -4
  41. data/test/helper_test.rb +8 -0
  42. data/test/internals_test.rb +25 -33
  43. data/test/lint/hashes.rb +17 -3
  44. data/test/lint/internals.rb +2 -3
  45. data/test/lint/lists.rb +17 -3
  46. data/test/lint/sets.rb +30 -6
  47. data/test/lint/sorted_sets.rb +56 -27
  48. data/test/lint/strings.rb +9 -13
  49. data/test/lint/value_types.rb +12 -15
  50. data/test/persistence_control_commands_test.rb +0 -1
  51. data/test/pipelining_commands_test.rb +69 -6
  52. data/test/publish_subscribe_test.rb +1 -1
  53. data/test/redis_mock.rb +14 -5
  54. data/test/remote_server_control_commands_test.rb +8 -2
  55. data/test/sorting_test.rb +0 -1
  56. data/test/test.conf +1 -0
  57. data/test/transactions_test.rb +88 -15
  58. data/test/unknown_commands_test.rb +1 -2
  59. data/test/url_param_test.rb +0 -1
  60. metadata +68 -16
  61. data/lib/redis/compat.rb +0 -21
data/lib/redis/client.rb CHANGED
@@ -1,8 +1,11 @@
1
+ require "redis/errors"
2
+
1
3
  class Redis
2
4
  class Client
3
5
  attr_accessor :db, :host, :port, :path, :password, :logger
4
6
  attr :timeout
5
7
  attr :connection
8
+ attr :command_map
6
9
 
7
10
  def initialize(options = {})
8
11
  @path = options[:path]
@@ -17,6 +20,7 @@ class Redis
17
20
  @logger = options[:logger]
18
21
  @reconnect = true
19
22
  @connection = Connection.drivers.last.new
23
+ @command_map = {}
20
24
  end
21
25
 
22
26
  def connect
@@ -34,43 +38,25 @@ class Redis
34
38
  @path || "#{@host}:#{@port}"
35
39
  end
36
40
 
37
- # Starting with 2.2.1, assume that this method is called with a single
38
- # array argument. Check its size for backwards compat.
39
- def call(*args)
40
- if args.first.is_a?(Array) && args.size == 1
41
- command = args.first
42
- else
43
- command = args
44
- end
45
-
41
+ def call(command, &block)
46
42
  reply = process([command]) { read }
47
- raise reply if reply.is_a?(RuntimeError)
48
- reply
49
- end
43
+ raise reply if reply.is_a?(CommandError)
50
44
 
51
- # Assume that this method is called with a single array argument. No
52
- # backwards compat here, since it was introduced in 2.2.2.
53
- def call_without_reply(command)
54
- process([command])
55
- nil
56
- end
57
-
58
- # Starting with 2.2.1, assume that this method is called with a single
59
- # array argument. Check its size for backwards compat.
60
- def call_loop(*args)
61
- if args.first.is_a?(Array) && args.size == 1
62
- command = args.first
45
+ if block
46
+ block.call(reply)
63
47
  else
64
- command = args
48
+ reply
65
49
  end
50
+ end
66
51
 
52
+ def call_loop(command)
67
53
  error = nil
68
54
 
69
55
  result = without_socket_timeout do
70
56
  process([command]) do
71
57
  loop do
72
58
  reply = read
73
- if reply.is_a?(RuntimeError)
59
+ if reply.is_a?(CommandError)
74
60
  error = reply
75
61
  break
76
62
  else
@@ -87,48 +73,67 @@ class Redis
87
73
  result
88
74
  end
89
75
 
90
- def call_pipelined(commands, options = {})
91
- options[:raise] = true unless options.has_key?(:raise)
76
+ def call_pipeline(pipeline)
77
+ without_reconnect_wrapper = lambda do |&blk| blk.call end
78
+ without_reconnect_wrapper = lambda do |&blk|
79
+ without_reconnect(&blk)
80
+ end if pipeline.without_reconnect?
81
+
82
+ shutdown_wrapper = lambda do |&blk| blk.call end
83
+ shutdown_wrapper = lambda do |&blk|
84
+ begin
85
+ blk.call
86
+ rescue ConnectionError
87
+ # Assume the pipeline was sent in one piece, but execution of
88
+ # SHUTDOWN caused none of the replies for commands that were executed
89
+ # prior to it from coming back around.
90
+ nil
91
+ end
92
+ end if pipeline.shutdown?
93
+
94
+ without_reconnect_wrapper.call do
95
+ shutdown_wrapper.call do
96
+ pipeline.finish(call_pipelined(pipeline.commands))
97
+ end
98
+ end
99
+ end
100
+
101
+ def call_pipelined(commands)
102
+ return [] if commands.empty?
92
103
 
93
104
  # The method #ensure_connected (called from #process) reconnects once on
94
105
  # I/O errors. To make an effort in making sure that commands are not
95
106
  # executed more than once, only allow reconnection before the first reply
96
107
  # has been read. When an error occurs after the first reply has been
97
- # read, retrying would re-execute the entire pipeline, thus re-issueing
98
- # already succesfully executed commands. To circumvent this, don't retry
99
- # after the first reply has been read succesfully.
100
- first = process(commands) { read }
101
- error = first if first.is_a?(RuntimeError)
108
+ # read, retrying would re-execute the entire pipeline, thus re-issuing
109
+ # already successfully executed commands. To circumvent this, don't retry
110
+ # after the first reply has been read successfully.
111
+
112
+ result = Array.new(commands.size)
113
+ reconnect = @reconnect
102
114
 
103
115
  begin
104
- remaining = commands.size - 1
105
- if remaining > 0
106
- replies = Array.new(remaining) do
107
- reply = read
108
- error ||= reply if reply.is_a?(RuntimeError)
109
- reply
116
+ process(commands) do
117
+ result[0] = read
118
+
119
+ @reconnect = false
120
+
121
+ (commands.size - 1).times do |i|
122
+ result[i + 1] = read
110
123
  end
111
- replies.unshift first
112
- replies
113
- else
114
- replies = [first]
115
124
  end
116
- rescue Exception
117
- disconnect
118
- raise
125
+ ensure
126
+ @reconnect = reconnect
119
127
  end
120
128
 
121
- # Raise first error in pipeline when we should raise.
122
- raise error if error && options[:raise]
123
-
124
- replies
129
+ result
125
130
  end
126
131
 
127
- def call_without_timeout(*args)
132
+ def call_without_timeout(command, &blk)
128
133
  without_socket_timeout do
129
- call(*args)
134
+ call(command, &blk)
130
135
  end
131
- rescue Errno::ECONNRESET
136
+ rescue ConnectionError
132
137
  retry
133
138
  end
134
139
 
@@ -136,6 +141,11 @@ class Redis
136
141
  logging(commands) do
137
142
  ensure_connected do
138
143
  commands.each do |command|
144
+ if command_map[command.first]
145
+ command = command.dup
146
+ command[0] = command_map[command.first]
147
+ end
148
+
139
149
  connection.write(command)
140
150
  end
141
151
 
@@ -157,20 +167,23 @@ class Redis
157
167
  connect
158
168
  end
159
169
 
170
+ def io
171
+ yield
172
+ rescue Errno::EAGAIN
173
+ raise TimeoutError, "Connection timed out"
174
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
175
+ raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
176
+ end
177
+
160
178
  def read
161
- begin
179
+ io do
162
180
  connection.read
181
+ end
182
+ end
163
183
 
164
- rescue Errno::EAGAIN
165
- # We want to make sure it reconnects on the next command after the
166
- # timeout. Otherwise the server may reply in the meantime leaving
167
- # the protocol in a desync status.
168
- disconnect
169
-
170
- raise Errno::EAGAIN, "Timeout reading from the socket"
171
-
172
- rescue Errno::ECONNRESET
173
- raise Errno::ECONNRESET, "Connection lost"
184
+ def write(command)
185
+ io do
186
+ connection.write(command)
174
187
  end
175
188
  end
176
189
 
@@ -232,8 +245,10 @@ class Redis
232
245
  # of seconds. This hack is from memcached ruby client.
233
246
  self.timeout = @timeout
234
247
 
248
+ rescue Timeout::Error
249
+ raise CannotConnectError, "Timed out connecting to Redis on #{location}"
235
250
  rescue Errno::ECONNREFUSED
236
- raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{location}"
251
+ raise CannotConnectError, "Error connecting to Redis on #{location} (ECONNREFUSED)"
237
252
  end
238
253
 
239
254
  def timeout=(timeout)
@@ -248,13 +263,13 @@ class Redis
248
263
  tries += 1
249
264
 
250
265
  yield
251
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL
266
+ rescue ConnectionError
252
267
  disconnect
253
268
 
254
269
  if tries < 2 && @reconnect
255
270
  retry
256
271
  else
257
- raise Errno::ECONNRESET
272
+ raise
258
273
  end
259
274
  rescue Exception
260
275
  disconnect
@@ -5,15 +5,24 @@ class Redis
5
5
  COMMAND_DELIMITER = "\r\n"
6
6
 
7
7
  def build_command(args)
8
- command = []
9
- command << "*#{args.size}"
8
+ command = [nil]
10
9
 
11
- args.each do |arg|
12
- arg = arg.to_s
13
- command << "$#{string_size arg}"
14
- command << arg
10
+ args.each do |i|
11
+ if i.is_a? Array
12
+ i.each do |j|
13
+ j = j.to_s
14
+ command << "$#{j.bytesize}"
15
+ command << j
16
+ end
17
+ else
18
+ i = i.to_s
19
+ command << "$#{i.bytesize}"
20
+ command << i
21
+ end
15
22
  end
16
23
 
24
+ command[0] = "*#{(command.length - 1) / 2}"
25
+
17
26
  # Trailing delimiter
18
27
  command << ""
19
28
  command.join(COMMAND_DELIMITER)
@@ -21,16 +30,6 @@ class Redis
21
30
 
22
31
  protected
23
32
 
24
- if "".respond_to?(:bytesize)
25
- def string_size(string)
26
- string.to_s.bytesize
27
- end
28
- else
29
- def string_size(string)
30
- string.to_s.size
31
- end
32
- end
33
-
34
33
  if defined?(Encoding::default_external)
35
34
  def encode(string)
36
35
  string.force_encoding(Encoding::default_external)
@@ -1,4 +1,5 @@
1
1
  require "redis/connection/registry"
2
+ require "redis/errors"
2
3
  require "hiredis/connection"
3
4
  require "timeout"
4
5
 
@@ -34,13 +35,15 @@ class Redis
34
35
  end
35
36
 
36
37
  def write(command)
37
- @connection.write(command)
38
+ @connection.write(command.flatten(1))
38
39
  end
39
40
 
40
41
  def read
41
- @connection.read
42
+ reply = @connection.read
43
+ reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
44
+ reply
42
45
  rescue RuntimeError => err
43
- raise ::Redis::ProtocolError.new(err.message)
46
+ raise ProtocolError.new(err.message)
44
47
  end
45
48
  end
46
49
  end
@@ -1,5 +1,6 @@
1
1
  require "redis/connection/registry"
2
2
  require "redis/connection/command_helper"
3
+ require "redis/errors"
3
4
  require "socket"
4
5
 
5
6
  class Redis
@@ -80,7 +81,7 @@ class Redis
80
81
  end
81
82
 
82
83
  def format_error_reply(line)
83
- RuntimeError.new(line.strip)
84
+ CommandError.new(line.strip)
84
85
  end
85
86
 
86
87
  def format_status_reply(line)
@@ -1,5 +1,6 @@
1
1
  require "redis/connection/command_helper"
2
2
  require "redis/connection/registry"
3
+ require "redis/errors"
3
4
  require "em-synchrony"
4
5
  require "hiredis/reader"
5
6
 
@@ -28,10 +29,11 @@ class Redis
28
29
 
29
30
  begin
30
31
  until (reply = @reader.gets) == false
32
+ reply = CommandError.new(reply.message) if reply.is_a?(RuntimeError)
31
33
  @req.succeed [:reply, reply]
32
34
  end
33
35
  rescue RuntimeError => err
34
- @req.fail [:error, ::Redis::ProtocolError.new(err.message)]
36
+ @req.fail [:error, ProtocolError.new(err.message)]
35
37
  end
36
38
  end
37
39
 
@@ -180,10 +180,6 @@ class Redis
180
180
  node_for(key).append(key, value)
181
181
  end
182
182
 
183
- def substr(key, start, stop)
184
- node_for(key).substr(key, start, stop)
185
- end
186
-
187
183
  def []=(key,value)
188
184
  set(key, value)
189
185
  end
@@ -208,7 +204,7 @@ class Redis
208
204
  end
209
205
 
210
206
  def mapped_mset(hash)
211
- mset(*hash.to_a.flatten)
207
+ raise CannotDistribute, :mapped_mset
212
208
  end
213
209
 
214
210
  # Set multiple keys to multiple values, only if none of the keys exist.
@@ -240,12 +236,12 @@ class Redis
240
236
  node_for(key).decrby(key, decrement)
241
237
  end
242
238
 
243
- # Append a value to a list.
239
+ # Append one or more values to a list.
244
240
  def rpush(key, value)
245
241
  node_for(key).rpush(key, value)
246
242
  end
247
243
 
248
- # Prepend a value to a list.
244
+ # Prepend one or more values to a list.
249
245
  def lpush(key, value)
250
246
  node_for(key).lpush(key, value)
251
247
  end
@@ -318,14 +314,14 @@ class Redis
318
314
  end
319
315
  end
320
316
 
321
- # Add a member to a set.
322
- def sadd(key, value)
323
- node_for(key).sadd(key, value)
317
+ # Add one or more members to a set.
318
+ def sadd(key, member)
319
+ node_for(key).sadd(key, member)
324
320
  end
325
321
 
326
- # Remove a member from a set.
327
- def srem(key, value)
328
- node_for(key).srem(key, value)
322
+ # Remove one or more members from a set.
323
+ def srem(key, member)
324
+ node_for(key).srem(key, member)
329
325
  end
330
326
 
331
327
  # Remove and return a random member from a set.
@@ -402,12 +398,13 @@ class Redis
402
398
  node_for(key).srandmember(key)
403
399
  end
404
400
 
405
- # Add a member to a sorted set, or update its score if it already exists.
406
- def zadd(key, score, member)
407
- node_for(key).zadd(key, score, member)
401
+ # Add one or more members to a sorted set, or update the score for members
402
+ # that already exist.
403
+ def zadd(key, *args)
404
+ node_for(key).zadd(key, *args)
408
405
  end
409
406
 
410
- # Remove a member from a sorted set.
407
+ # Remove one or more members from a sorted set.
411
408
  def zrem(key, member)
412
409
  node_for(key).zrem(key, member)
413
410
  end
@@ -465,6 +462,11 @@ class Redis
465
462
  node_for(key).zcard(key)
466
463
  end
467
464
 
465
+ # Get the number of members in a particular score range.
466
+ def zcount(key, min, max)
467
+ node_for(key).zcount(key, min, max)
468
+ end
469
+
468
470
  # Get the score associated with the given member in a sorted set.
469
471
  def zscore(key, member)
470
472
  node_for(key).zscore(key, member)
@@ -500,7 +502,7 @@ class Redis
500
502
  node_for(key).hget(key, field)
501
503
  end
502
504
 
503
- # Delete a hash field.
505
+ # Delete one or more hash fields.
504
506
  def hdel(key, field)
505
507
  node_for(key).hdel(key, field)
506
508
  end
@@ -0,0 +1,38 @@
1
+ class Redis
2
+ # Base error for all redis-rb errors.
3
+ class BaseError < RuntimeError
4
+ end
5
+
6
+ # Raised by the connection when a protocol error occurs.
7
+ class ProtocolError < BaseError
8
+ def initialize(reply_type)
9
+ super(<<-EOS.gsub(/(?:^|\n)\s*/, " "))
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
+ If you're in a forking environment, such as Unicorn, you need to
14
+ connect to Redis after forking.
15
+ EOS
16
+ end
17
+ end
18
+
19
+ # Raised by the client when command execution returns an error reply.
20
+ class CommandError < BaseError
21
+ end
22
+
23
+ # Base error for connection related errors.
24
+ class BaseConnectionError < BaseError
25
+ end
26
+
27
+ # Raised when connection to a Redis server cannot be made.
28
+ class CannotConnectError < BaseConnectionError
29
+ end
30
+
31
+ # Raised when connection to a Redis server is lost.
32
+ class ConnectionError < BaseConnectionError
33
+ end
34
+
35
+ # Raised when performing I/O times out.
36
+ class TimeoutError < BaseConnectionError
37
+ end
38
+ end