redis 2.2.2 → 3.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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