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.
- 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
|