redis 2.1.1 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +8 -0
- data/CHANGELOG.md +34 -0
- data/README.md +190 -0
- data/Rakefile +194 -79
- data/benchmarking/logging.rb +62 -0
- data/benchmarking/pipeline.rb +51 -0
- data/benchmarking/speed.rb +21 -0
- data/benchmarking/suite.rb +24 -0
- data/benchmarking/thread_safety.rb +38 -0
- data/benchmarking/worker.rb +71 -0
- data/examples/basic.rb +15 -0
- data/examples/dist_redis.rb +43 -0
- data/examples/incr-decr.rb +17 -0
- data/examples/list.rb +26 -0
- data/examples/pubsub.rb +31 -0
- data/examples/sets.rb +36 -0
- data/examples/unicorn/config.ru +3 -0
- data/examples/unicorn/unicorn.rb +20 -0
- data/lib/redis.rb +612 -156
- data/lib/redis/client.rb +98 -57
- data/lib/redis/connection.rb +9 -134
- data/lib/redis/connection/command_helper.rb +45 -0
- data/lib/redis/connection/hiredis.rb +49 -0
- data/lib/redis/connection/registry.rb +12 -0
- data/lib/redis/connection/ruby.rb +131 -0
- data/lib/redis/connection/synchrony.rb +125 -0
- data/lib/redis/distributed.rb +161 -5
- data/lib/redis/pipeline.rb +6 -0
- data/lib/redis/version.rb +3 -0
- data/redis.gemspec +24 -0
- data/test/commands_on_hashes_test.rb +32 -0
- data/test/commands_on_lists_test.rb +60 -0
- data/test/commands_on_sets_test.rb +78 -0
- data/test/commands_on_sorted_sets_test.rb +109 -0
- data/test/commands_on_strings_test.rb +80 -0
- data/test/commands_on_value_types_test.rb +88 -0
- data/test/connection_handling_test.rb +87 -0
- data/test/db/.gitignore +1 -0
- data/test/distributed_blocking_commands_test.rb +53 -0
- data/test/distributed_commands_on_hashes_test.rb +12 -0
- data/test/distributed_commands_on_lists_test.rb +24 -0
- data/test/distributed_commands_on_sets_test.rb +85 -0
- data/test/distributed_commands_on_strings_test.rb +50 -0
- data/test/distributed_commands_on_value_types_test.rb +73 -0
- data/test/distributed_commands_requiring_clustering_test.rb +148 -0
- data/test/distributed_connection_handling_test.rb +25 -0
- data/test/distributed_internals_test.rb +18 -0
- data/test/distributed_key_tags_test.rb +53 -0
- data/test/distributed_persistence_control_commands_test.rb +24 -0
- data/test/distributed_publish_subscribe_test.rb +101 -0
- data/test/distributed_remote_server_control_commands_test.rb +31 -0
- data/test/distributed_sorting_test.rb +21 -0
- data/test/distributed_test.rb +60 -0
- data/test/distributed_transactions_test.rb +34 -0
- data/test/encoding_test.rb +16 -0
- data/test/error_replies_test.rb +53 -0
- data/test/helper.rb +145 -0
- data/test/internals_test.rb +157 -0
- data/test/lint/hashes.rb +114 -0
- data/test/lint/internals.rb +41 -0
- data/test/lint/lists.rb +93 -0
- data/test/lint/sets.rb +66 -0
- data/test/lint/sorted_sets.rb +167 -0
- data/test/lint/strings.rb +137 -0
- data/test/lint/value_types.rb +84 -0
- data/test/persistence_control_commands_test.rb +22 -0
- data/test/pipelining_commands_test.rb +123 -0
- data/test/publish_subscribe_test.rb +158 -0
- data/test/redis_mock.rb +80 -0
- data/test/remote_server_control_commands_test.rb +63 -0
- data/test/sorting_test.rb +44 -0
- data/test/synchrony_driver.rb +57 -0
- data/test/test.conf +8 -0
- data/test/thread_safety_test.rb +30 -0
- data/test/transactions_test.rb +100 -0
- data/test/unknown_commands_test.rb +14 -0
- data/test/url_param_test.rb +60 -0
- metadata +128 -19
- data/README.markdown +0 -129
data/lib/redis/client.rb
CHANGED
@@ -1,48 +1,104 @@
|
|
1
1
|
class Redis
|
2
2
|
class Client
|
3
|
-
attr_accessor :db, :host, :port, :password, :logger
|
3
|
+
attr_accessor :db, :host, :port, :path, :password, :logger
|
4
4
|
attr :timeout
|
5
5
|
attr :connection
|
6
6
|
|
7
7
|
def initialize(options = {})
|
8
|
-
@
|
9
|
-
@
|
8
|
+
@path = options[:path]
|
9
|
+
if @path.nil?
|
10
|
+
@host = options[:host] || "127.0.0.1"
|
11
|
+
@port = (options[:port] || 6379).to_i
|
12
|
+
end
|
13
|
+
|
10
14
|
@db = (options[:db] || 0).to_i
|
11
|
-
@timeout = (options[:timeout] || 5).
|
15
|
+
@timeout = (options[:timeout] || 5).to_f
|
12
16
|
@password = options[:password]
|
13
17
|
@logger = options[:logger]
|
14
|
-
@
|
18
|
+
@reconnect = true
|
19
|
+
@connection = Connection.drivers.last.new
|
15
20
|
end
|
16
21
|
|
17
22
|
def connect
|
18
|
-
|
23
|
+
establish_connection
|
19
24
|
call(:auth, @password) if @password
|
20
25
|
call(:select, @db) if @db != 0
|
21
26
|
self
|
22
27
|
end
|
23
28
|
|
24
29
|
def id
|
25
|
-
"redis://#{
|
30
|
+
"redis://#{location}/#{db}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def location
|
34
|
+
@path || "#{@host}:#{@port}"
|
26
35
|
end
|
27
36
|
|
28
37
|
def call(*args)
|
29
|
-
process(args)
|
30
|
-
|
31
|
-
|
38
|
+
reply = process(args) { read }
|
39
|
+
raise reply if reply.is_a?(RuntimeError)
|
40
|
+
reply
|
32
41
|
end
|
33
42
|
|
34
43
|
def call_loop(*args)
|
35
|
-
|
44
|
+
error = nil
|
45
|
+
|
46
|
+
result = without_socket_timeout do
|
36
47
|
process(args) do
|
37
|
-
loop
|
48
|
+
loop do
|
49
|
+
reply = read
|
50
|
+
if reply.is_a?(RuntimeError)
|
51
|
+
error = reply
|
52
|
+
break
|
53
|
+
else
|
54
|
+
yield reply
|
55
|
+
end
|
56
|
+
end
|
38
57
|
end
|
39
58
|
end
|
59
|
+
|
60
|
+
# Raise error when previous block broke out of the loop.
|
61
|
+
raise error if error
|
62
|
+
|
63
|
+
# Result is set to the value that the provided block used to break.
|
64
|
+
result
|
40
65
|
end
|
41
66
|
|
42
|
-
def call_pipelined(commands)
|
43
|
-
|
44
|
-
|
67
|
+
def call_pipelined(commands, options = {})
|
68
|
+
options[:raise] = true unless options.has_key?(:raise)
|
69
|
+
|
70
|
+
# The method #ensure_connected (called from #process) reconnects once on
|
71
|
+
# I/O errors. To make an effort in making sure that commands are not
|
72
|
+
# executed more than once, only allow reconnection before the first reply
|
73
|
+
# has been read. When an error occurs after the first reply has been
|
74
|
+
# read, retrying would re-execute the entire pipeline, thus re-issueing
|
75
|
+
# already succesfully executed commands. To circumvent this, don't retry
|
76
|
+
# after the first reply has been read succesfully.
|
77
|
+
first = process(*commands) { read }
|
78
|
+
error = first if first.is_a?(RuntimeError)
|
79
|
+
|
80
|
+
begin
|
81
|
+
remaining = commands.size - 1
|
82
|
+
if remaining > 0
|
83
|
+
replies = Array.new(remaining) do
|
84
|
+
reply = read
|
85
|
+
error ||= reply if reply.is_a?(RuntimeError)
|
86
|
+
reply
|
87
|
+
end
|
88
|
+
replies.unshift first
|
89
|
+
replies
|
90
|
+
else
|
91
|
+
replies = [first]
|
92
|
+
end
|
93
|
+
rescue Exception
|
94
|
+
disconnect
|
95
|
+
raise
|
45
96
|
end
|
97
|
+
|
98
|
+
# Raise first error in pipeline when we should raise.
|
99
|
+
raise error if error && options[:raise]
|
100
|
+
|
101
|
+
replies
|
46
102
|
end
|
47
103
|
|
48
104
|
def call_without_timeout(*args)
|
@@ -106,6 +162,15 @@ class Redis
|
|
106
162
|
end
|
107
163
|
end
|
108
164
|
|
165
|
+
def without_reconnect
|
166
|
+
begin
|
167
|
+
original, @reconnect = @reconnect, false
|
168
|
+
yield
|
169
|
+
ensure
|
170
|
+
@reconnect = original
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
109
174
|
protected
|
110
175
|
|
111
176
|
def deprecated(old, new = nil, trace = caller[0])
|
@@ -129,9 +194,14 @@ class Redis
|
|
129
194
|
end
|
130
195
|
end
|
131
196
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
197
|
+
def establish_connection
|
198
|
+
# Need timeout in usecs, like socket timeout.
|
199
|
+
timeout = Integer(@timeout * 1_000_000)
|
200
|
+
|
201
|
+
if @path
|
202
|
+
connection.connect_unix(@path, timeout)
|
203
|
+
else
|
204
|
+
connection.connect(@host, @port, timeout)
|
135
205
|
end
|
136
206
|
|
137
207
|
# If the timeout is set we set the low level socket options in order
|
@@ -140,7 +210,7 @@ class Redis
|
|
140
210
|
self.timeout = @timeout
|
141
211
|
|
142
212
|
rescue Errno::ECONNREFUSED
|
143
|
-
raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{
|
213
|
+
raise Errno::ECONNREFUSED, "Unable to connect to Redis on #{location}"
|
144
214
|
end
|
145
215
|
|
146
216
|
def timeout=(timeout)
|
@@ -148,13 +218,18 @@ class Redis
|
|
148
218
|
end
|
149
219
|
|
150
220
|
def ensure_connected
|
151
|
-
|
221
|
+
tries = 0
|
152
222
|
|
153
223
|
begin
|
224
|
+
connect unless connected?
|
225
|
+
tries += 1
|
226
|
+
|
154
227
|
yield
|
155
|
-
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF
|
156
|
-
|
157
|
-
|
228
|
+
rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL
|
229
|
+
disconnect
|
230
|
+
|
231
|
+
if tries < 2 && @reconnect
|
232
|
+
retry
|
158
233
|
else
|
159
234
|
raise Errno::ECONNRESET
|
160
235
|
end
|
@@ -163,39 +238,5 @@ class Redis
|
|
163
238
|
raise
|
164
239
|
end
|
165
240
|
end
|
166
|
-
|
167
|
-
class ThreadSafe < self
|
168
|
-
def initialize(*args)
|
169
|
-
require "monitor"
|
170
|
-
|
171
|
-
super(*args)
|
172
|
-
@mutex = ::Monitor.new
|
173
|
-
end
|
174
|
-
|
175
|
-
def synchronize(&block)
|
176
|
-
@mutex.synchronize(&block)
|
177
|
-
end
|
178
|
-
|
179
|
-
def ensure_connected(&block)
|
180
|
-
synchronize { super }
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
begin
|
185
|
-
require "system_timer"
|
186
|
-
|
187
|
-
def with_timeout(seconds, &block)
|
188
|
-
SystemTimer.timeout_after(seconds, &block)
|
189
|
-
end
|
190
|
-
|
191
|
-
rescue LoadError
|
192
|
-
warn "WARNING: using the built-in Timeout class which is known to have issues when used for opening connections. Install the SystemTimer gem if you want to make sure the Redis client will not hang." unless RUBY_VERSION >= "1.9" || RUBY_PLATFORM =~ /java/
|
193
|
-
|
194
|
-
require "timeout"
|
195
|
-
|
196
|
-
def with_timeout(seconds, &block)
|
197
|
-
Timeout.timeout(seconds, &block)
|
198
|
-
end
|
199
|
-
end
|
200
241
|
end
|
201
242
|
end
|
data/lib/redis/connection.rb
CHANGED
@@ -1,134 +1,9 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@sock = nil
|
11
|
-
end
|
12
|
-
|
13
|
-
def connected?
|
14
|
-
!! @sock
|
15
|
-
end
|
16
|
-
|
17
|
-
def connect(host, port)
|
18
|
-
@sock = TCPSocket.new(host, port)
|
19
|
-
@sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
|
20
|
-
end
|
21
|
-
|
22
|
-
def disconnect
|
23
|
-
@sock.close
|
24
|
-
rescue
|
25
|
-
ensure
|
26
|
-
@sock = nil
|
27
|
-
end
|
28
|
-
|
29
|
-
def timeout=(usecs)
|
30
|
-
secs = Integer(usecs / 1_000_000)
|
31
|
-
usecs = Integer(usecs - (secs * 1_000_000)) # 0 - 999_999
|
32
|
-
|
33
|
-
optval = [secs, usecs].pack("l_2")
|
34
|
-
|
35
|
-
begin
|
36
|
-
@sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
|
37
|
-
@sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
|
38
|
-
rescue Errno::ENOPROTOOPT
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
COMMAND_DELIMITER = "\r\n"
|
43
|
-
|
44
|
-
def write(command)
|
45
|
-
@sock.write(build_command(*command).join(COMMAND_DELIMITER))
|
46
|
-
@sock.write(COMMAND_DELIMITER)
|
47
|
-
end
|
48
|
-
|
49
|
-
def build_command(name, *args)
|
50
|
-
command = []
|
51
|
-
command << "*#{args.size + 1}"
|
52
|
-
command << "$#{string_size name}"
|
53
|
-
command << name
|
54
|
-
|
55
|
-
args.each do |arg|
|
56
|
-
arg = arg.to_s
|
57
|
-
command << "$#{string_size arg}"
|
58
|
-
command << arg
|
59
|
-
end
|
60
|
-
|
61
|
-
command
|
62
|
-
end
|
63
|
-
|
64
|
-
def read
|
65
|
-
# We read the first byte using read() mainly because gets() is
|
66
|
-
# immune to raw socket timeouts.
|
67
|
-
reply_type = @sock.read(1)
|
68
|
-
|
69
|
-
raise Errno::ECONNRESET unless reply_type
|
70
|
-
|
71
|
-
format_reply(reply_type, @sock.gets)
|
72
|
-
end
|
73
|
-
|
74
|
-
def format_reply(reply_type, line)
|
75
|
-
case reply_type
|
76
|
-
when MINUS then format_error_reply(line)
|
77
|
-
when PLUS then format_status_reply(line)
|
78
|
-
when COLON then format_integer_reply(line)
|
79
|
-
when DOLLAR then format_bulk_reply(line)
|
80
|
-
when ASTERISK then format_multi_bulk_reply(line)
|
81
|
-
else raise ProtocolError.new(reply_type)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
def format_error_reply(line)
|
86
|
-
raise "-" + line.strip
|
87
|
-
end
|
88
|
-
|
89
|
-
def format_status_reply(line)
|
90
|
-
line.strip
|
91
|
-
end
|
92
|
-
|
93
|
-
def format_integer_reply(line)
|
94
|
-
line.to_i
|
95
|
-
end
|
96
|
-
|
97
|
-
def format_bulk_reply(line)
|
98
|
-
bulklen = line.to_i
|
99
|
-
return if bulklen == -1
|
100
|
-
reply = encode(@sock.read(bulklen))
|
101
|
-
@sock.read(2) # Discard CRLF.
|
102
|
-
reply
|
103
|
-
end
|
104
|
-
|
105
|
-
def format_multi_bulk_reply(line)
|
106
|
-
n = line.to_i
|
107
|
-
return if n == -1
|
108
|
-
|
109
|
-
Array.new(n) { read }
|
110
|
-
end
|
111
|
-
|
112
|
-
protected
|
113
|
-
|
114
|
-
if "".respond_to?(:bytesize)
|
115
|
-
def string_size(string)
|
116
|
-
string.to_s.bytesize
|
117
|
-
end
|
118
|
-
else
|
119
|
-
def string_size(string)
|
120
|
-
string.to_s.size
|
121
|
-
end
|
122
|
-
end
|
123
|
-
|
124
|
-
if defined?(Encoding::default_external)
|
125
|
-
def encode(string)
|
126
|
-
string.force_encoding(Encoding::default_external)
|
127
|
-
end
|
128
|
-
else
|
129
|
-
def encode(string)
|
130
|
-
string
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
1
|
+
require "redis/connection/registry"
|
2
|
+
|
3
|
+
# If a connection driver was required before this file, the array
|
4
|
+
# Redis::Connection.drivers will contain one or more classes. The last driver
|
5
|
+
# in this array will be used as default driver. If this array is empty, we load
|
6
|
+
# the plain Ruby driver as our default. Another driver can be required at a
|
7
|
+
# later point in time, causing it to be the last element of the #drivers array
|
8
|
+
# and therefore be chosen by default.
|
9
|
+
require "redis/connection/ruby" if Redis::Connection.drivers.empty?
|
@@ -0,0 +1,45 @@
|
|
1
|
+
class Redis
|
2
|
+
module Connection
|
3
|
+
module CommandHelper
|
4
|
+
|
5
|
+
COMMAND_DELIMITER = "\r\n"
|
6
|
+
|
7
|
+
def build_command(*args)
|
8
|
+
command = []
|
9
|
+
command << "*#{args.size}"
|
10
|
+
|
11
|
+
args.each do |arg|
|
12
|
+
arg = arg.to_s
|
13
|
+
command << "$#{string_size arg}"
|
14
|
+
command << arg
|
15
|
+
end
|
16
|
+
|
17
|
+
# Trailing delimiter
|
18
|
+
command << ""
|
19
|
+
command
|
20
|
+
end
|
21
|
+
|
22
|
+
protected
|
23
|
+
|
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
|
+
if defined?(Encoding::default_external)
|
35
|
+
def encode(string)
|
36
|
+
string.force_encoding(Encoding::default_external)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
def encode(string)
|
40
|
+
string
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "redis/connection/registry"
|
2
|
+
require "hiredis/connection"
|
3
|
+
require "timeout"
|
4
|
+
|
5
|
+
class Redis
|
6
|
+
module Connection
|
7
|
+
class Hiredis
|
8
|
+
def initialize
|
9
|
+
@connection = ::Hiredis::Connection.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def connected?
|
13
|
+
@connection.connected?
|
14
|
+
end
|
15
|
+
|
16
|
+
def timeout=(usecs)
|
17
|
+
@connection.timeout = usecs
|
18
|
+
end
|
19
|
+
|
20
|
+
def connect(host, port, timeout)
|
21
|
+
@connection.connect(host, port, timeout)
|
22
|
+
rescue Errno::ETIMEDOUT
|
23
|
+
raise Timeout::Error
|
24
|
+
end
|
25
|
+
|
26
|
+
def connect_unix(path, timeout)
|
27
|
+
@connection.connect_unix(path, timeout)
|
28
|
+
rescue Errno::ETIMEDOUT
|
29
|
+
raise Timeout::Error
|
30
|
+
end
|
31
|
+
|
32
|
+
def disconnect
|
33
|
+
@connection.disconnect
|
34
|
+
end
|
35
|
+
|
36
|
+
def write(command)
|
37
|
+
@connection.write(command)
|
38
|
+
end
|
39
|
+
|
40
|
+
def read
|
41
|
+
@connection.read
|
42
|
+
rescue RuntimeError => err
|
43
|
+
raise ::Redis::ProtocolError.new(err.message)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Redis::Connection.drivers << Redis::Connection::Hiredis
|