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.
- data/.gitignore +2 -0
- data/.yardopts +3 -0
- data/CHANGELOG.md +65 -1
- data/README.md +6 -0
- data/Rakefile +19 -27
- data/lib/redis.rb +737 -170
- data/lib/redis/client.rb +82 -67
- data/lib/redis/connection/command_helper.rb +15 -16
- data/lib/redis/connection/hiredis.rb +6 -3
- data/lib/redis/connection/ruby.rb +2 -1
- data/lib/redis/connection/synchrony.rb +3 -1
- data/lib/redis/distributed.rb +20 -18
- data/lib/redis/errors.rb +38 -0
- data/lib/redis/hash_ring.rb +2 -2
- data/lib/redis/pipeline.rb +91 -19
- data/lib/redis/subscribe.rb +1 -16
- data/lib/redis/version.rb +1 -1
- data/redis.gemspec +30 -11
- data/test/command_map_test.rb +29 -0
- data/test/commands_on_hashes_test.rb +3 -3
- data/test/commands_on_lists_test.rb +1 -1
- data/test/commands_on_sets_test.rb +0 -2
- data/test/commands_on_sorted_sets_test.rb +8 -9
- data/test/commands_on_strings_test.rb +3 -3
- data/test/commands_on_value_types_test.rb +0 -1
- data/test/connection_handling_test.rb +120 -4
- data/test/distributed_commands_on_hashes_test.rb +0 -1
- data/test/distributed_commands_on_lists_test.rb +0 -1
- data/test/distributed_commands_on_sets_test.rb +0 -1
- data/test/distributed_commands_on_sorted_sets_test.rb +19 -0
- data/test/distributed_commands_on_strings_test.rb +0 -1
- data/test/distributed_commands_on_value_types_test.rb +0 -1
- data/test/distributed_connection_handling_test.rb +0 -1
- data/test/distributed_key_tags_test.rb +0 -1
- data/test/distributed_persistence_control_commands_test.rb +0 -1
- data/test/distributed_publish_subscribe_test.rb +1 -2
- data/test/distributed_remote_server_control_commands_test.rb +2 -3
- data/test/distributed_transactions_test.rb +0 -1
- data/test/encoding_test.rb +0 -1
- data/test/helper.rb +14 -4
- data/test/helper_test.rb +8 -0
- data/test/internals_test.rb +25 -33
- data/test/lint/hashes.rb +17 -3
- data/test/lint/internals.rb +2 -3
- data/test/lint/lists.rb +17 -3
- data/test/lint/sets.rb +30 -6
- data/test/lint/sorted_sets.rb +56 -27
- data/test/lint/strings.rb +9 -13
- data/test/lint/value_types.rb +12 -15
- data/test/persistence_control_commands_test.rb +0 -1
- data/test/pipelining_commands_test.rb +69 -6
- data/test/publish_subscribe_test.rb +1 -1
- data/test/redis_mock.rb +14 -5
- data/test/remote_server_control_commands_test.rb +8 -2
- data/test/sorting_test.rb +0 -1
- data/test/test.conf +1 -0
- data/test/transactions_test.rb +88 -15
- data/test/unknown_commands_test.rb +1 -2
- data/test/url_param_test.rb +0 -1
- metadata +68 -16
- 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
|
-
|
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?(
|
48
|
-
reply
|
49
|
-
end
|
43
|
+
raise reply if reply.is_a?(CommandError)
|
50
44
|
|
51
|
-
|
52
|
-
|
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
|
-
|
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?(
|
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
|
91
|
-
|
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-
|
98
|
-
# already
|
99
|
-
# after the first reply has been read
|
100
|
-
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
-
|
117
|
-
|
118
|
-
raise
|
125
|
+
ensure
|
126
|
+
@reconnect = reconnect
|
119
127
|
end
|
120
128
|
|
121
|
-
|
122
|
-
raise error if error && options[:raise]
|
123
|
-
|
124
|
-
replies
|
129
|
+
result
|
125
130
|
end
|
126
131
|
|
127
|
-
def call_without_timeout(
|
132
|
+
def call_without_timeout(command, &blk)
|
128
133
|
without_socket_timeout do
|
129
|
-
call(
|
134
|
+
call(command, &blk)
|
130
135
|
end
|
131
|
-
rescue
|
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
|
-
|
179
|
+
io do
|
162
180
|
connection.read
|
181
|
+
end
|
182
|
+
end
|
163
183
|
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|
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
|
266
|
+
rescue ConnectionError
|
252
267
|
disconnect
|
253
268
|
|
254
269
|
if tries < 2 && @reconnect
|
255
270
|
retry
|
256
271
|
else
|
257
|
-
raise
|
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 |
|
12
|
-
|
13
|
-
|
14
|
-
|
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
|
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
|
-
|
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,
|
36
|
+
@req.fail [:error, ProtocolError.new(err.message)]
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
data/lib/redis/distributed.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
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
|
322
|
-
def sadd(key,
|
323
|
-
node_for(key).sadd(key,
|
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
|
327
|
-
def srem(key,
|
328
|
-
node_for(key).srem(key,
|
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
|
406
|
-
|
407
|
-
|
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
|
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
|
505
|
+
# Delete one or more hash fields.
|
504
506
|
def hdel(key, field)
|
505
507
|
node_for(key).hdel(key, field)
|
506
508
|
end
|
data/lib/redis/errors.rb
ADDED
@@ -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
|