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/test/helper.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
$:.unshift File.expand_path('../lib', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
require "cutest"
|
4
|
+
require "logger"
|
5
|
+
require "stringio"
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "ruby-debug"
|
9
|
+
rescue LoadError
|
10
|
+
end
|
11
|
+
|
12
|
+
PORT = 6379
|
13
|
+
OPTIONS = {:port => PORT, :db => 15, :timeout => 3}
|
14
|
+
NODES = ["redis://127.0.0.1:6379/15"]
|
15
|
+
|
16
|
+
def init(redis)
|
17
|
+
begin
|
18
|
+
redis.flushdb
|
19
|
+
redis.select 14
|
20
|
+
redis.flushdb
|
21
|
+
redis.select 15
|
22
|
+
redis
|
23
|
+
rescue Errno::ECONNREFUSED
|
24
|
+
puts <<-EOS
|
25
|
+
|
26
|
+
Cannot connect to Redis.
|
27
|
+
|
28
|
+
Make sure Redis is running on localhost, port 6379.
|
29
|
+
This testing suite connects to the database 15.
|
30
|
+
|
31
|
+
To install redis:
|
32
|
+
visit <http://code.google.com/p/redis/>.
|
33
|
+
|
34
|
+
To start the server:
|
35
|
+
rake start
|
36
|
+
|
37
|
+
To stop the server:
|
38
|
+
rake stop
|
39
|
+
|
40
|
+
EOS
|
41
|
+
exit 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
$VERBOSE = true
|
46
|
+
|
47
|
+
require "redis"
|
48
|
+
|
49
|
+
def driver
|
50
|
+
Redis::Connection.drivers.last.to_s.split("::").last.downcase.to_sym
|
51
|
+
end
|
52
|
+
|
53
|
+
if driver == :synchrony
|
54
|
+
# Make cutest fiber + eventmachine aware if the synchrony driver is used.
|
55
|
+
undef test if defined? test
|
56
|
+
def test(name = nil, &block)
|
57
|
+
cutest[:test] = name
|
58
|
+
|
59
|
+
blk = Proc.new do
|
60
|
+
prepare.each { |blk| blk.call }
|
61
|
+
block.call(setup && setup.call)
|
62
|
+
end
|
63
|
+
|
64
|
+
t = Thread.current[:cutest]
|
65
|
+
if defined? EventMachine
|
66
|
+
EM.synchrony do
|
67
|
+
Thread.current[:cutest] = t
|
68
|
+
blk.call
|
69
|
+
EM.stop
|
70
|
+
end
|
71
|
+
else
|
72
|
+
blk.call
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class Wire < Fiber
|
77
|
+
# We cannot run this fiber explicitly because EM schedules it. Resuming the
|
78
|
+
# current fiber on the next tick to let the reactor do work.
|
79
|
+
def self.pass
|
80
|
+
f = Fiber.current
|
81
|
+
EM.next_tick { f.resume }
|
82
|
+
Fiber.yield
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.sleep(sec)
|
86
|
+
EM::Synchrony.sleep(sec)
|
87
|
+
end
|
88
|
+
|
89
|
+
def initialize(&blk)
|
90
|
+
super
|
91
|
+
|
92
|
+
# Schedule run in next tick
|
93
|
+
EM.next_tick { resume }
|
94
|
+
end
|
95
|
+
|
96
|
+
def join
|
97
|
+
self.class.pass while alive?
|
98
|
+
end
|
99
|
+
end
|
100
|
+
else
|
101
|
+
class Wire < Thread
|
102
|
+
def self.sleep(sec)
|
103
|
+
Kernel.sleep(sec)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def capture_stderr
|
109
|
+
stderr = $stderr
|
110
|
+
$stderr = StringIO.new
|
111
|
+
|
112
|
+
yield
|
113
|
+
|
114
|
+
$stderr = stderr
|
115
|
+
end
|
116
|
+
|
117
|
+
def silent
|
118
|
+
verbose, $VERBOSE = $VERBOSE, false
|
119
|
+
|
120
|
+
begin
|
121
|
+
yield
|
122
|
+
ensure
|
123
|
+
$VERBOSE = verbose
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def with_external_encoding(encoding)
|
128
|
+
original_encoding = Encoding.default_external
|
129
|
+
|
130
|
+
begin
|
131
|
+
silent { Encoding.default_external = Encoding.find(encoding) }
|
132
|
+
yield
|
133
|
+
ensure
|
134
|
+
silent { Encoding.default_external = original_encoding }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def assert_nothing_raised(*exceptions)
|
139
|
+
begin
|
140
|
+
yield
|
141
|
+
rescue *exceptions
|
142
|
+
flunk(caller[1])
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
@@ -0,0 +1,157 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
require File.expand_path("./helper", File.dirname(__FILE__))
|
4
|
+
require File.expand_path("./redis_mock", File.dirname(__FILE__))
|
5
|
+
|
6
|
+
include RedisMock::Helper
|
7
|
+
|
8
|
+
setup do
|
9
|
+
log = StringIO.new
|
10
|
+
|
11
|
+
[Redis.new(OPTIONS.merge(:logger => ::Logger.new(log))), log]
|
12
|
+
end
|
13
|
+
|
14
|
+
$TEST_PIPELINING = true
|
15
|
+
$TEST_INSPECT = true
|
16
|
+
|
17
|
+
load File.expand_path("./lint/internals.rb", File.dirname(__FILE__))
|
18
|
+
|
19
|
+
test "Redis.current" do
|
20
|
+
Redis.current.set("foo", "bar")
|
21
|
+
|
22
|
+
assert "bar" == Redis.current.get("foo")
|
23
|
+
|
24
|
+
Redis.current = Redis.new(OPTIONS.merge(:db => 14))
|
25
|
+
|
26
|
+
assert Redis.current.get("foo").nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
test "Timeout" do
|
30
|
+
assert_nothing_raised do
|
31
|
+
Redis.new(OPTIONS.merge(:timeout => 0))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Don't use assert_raise because Timeour::Error in 1.8 inherits
|
36
|
+
# Exception instead of StandardError (on 1.9).
|
37
|
+
test "Connection timeout" do
|
38
|
+
# EM immediately raises CONNREFUSED
|
39
|
+
next if driver == :synchrony
|
40
|
+
|
41
|
+
result = false
|
42
|
+
|
43
|
+
begin
|
44
|
+
Redis.new(OPTIONS.merge(:host => "10.255.255.254", :timeout => 0.1)).ping
|
45
|
+
rescue Timeout::Error
|
46
|
+
result = true
|
47
|
+
ensure
|
48
|
+
assert result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
test "Retry when first read raises ECONNRESET" do
|
53
|
+
$request = 0
|
54
|
+
|
55
|
+
command = lambda do
|
56
|
+
case ($request += 1)
|
57
|
+
when 1; nil # Close on first command
|
58
|
+
else "+%d" % $request
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
redis_mock(:ping => command) do
|
63
|
+
redis = Redis.connect(:port => 6380, :timeout => 0.1)
|
64
|
+
assert "2" == redis.ping
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
test "Don't retry when wrapped inside #without_reconnect" do
|
69
|
+
$request = 0
|
70
|
+
|
71
|
+
command = lambda do
|
72
|
+
case ($request += 1)
|
73
|
+
when 1; nil # Close on first command
|
74
|
+
else "+%d" % $request
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
redis_mock(:ping => command) do
|
79
|
+
redis = Redis.connect(:port => 6380, :timeout => 0.1)
|
80
|
+
assert_raise Errno::ECONNRESET do
|
81
|
+
redis.without_reconnect do
|
82
|
+
redis.ping
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
assert !redis.client.connected?
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
test "Retry only once when read raises ECONNRESET" do
|
91
|
+
$request = 0
|
92
|
+
|
93
|
+
command = lambda do
|
94
|
+
case ($request += 1)
|
95
|
+
when 1; nil # Close on first command
|
96
|
+
when 2; nil # Close on second command
|
97
|
+
else "+%d" % $request
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
redis_mock(:ping => command) do
|
102
|
+
redis = Redis.connect(:port => 6380, :timeout => 0.1)
|
103
|
+
assert_raise Errno::ECONNRESET do
|
104
|
+
redis.ping
|
105
|
+
end
|
106
|
+
|
107
|
+
assert !redis.client.connected?
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
test "Don't retry when second read in pipeline raises ECONNRESET" do
|
112
|
+
$request = 0
|
113
|
+
|
114
|
+
command = lambda do
|
115
|
+
case ($request += 1)
|
116
|
+
when 2; nil # Close on second command
|
117
|
+
else "+%d" % $request
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
redis_mock(:ping => command) do
|
122
|
+
redis = Redis.connect(:port => 6380, :timeout => 0.1)
|
123
|
+
assert_raise Errno::ECONNRESET do
|
124
|
+
redis.pipelined do
|
125
|
+
redis.ping
|
126
|
+
redis.ping # Second #read times out
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
test "Connecting to UNIX domain socket" do
|
133
|
+
assert_nothing_raised do
|
134
|
+
Redis.new(OPTIONS.merge(:path => "/tmp/redis.sock")).ping
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# Using a mock server in a thread doesn't work here (possibly because blocking
|
139
|
+
# socket ops, raw socket timeouts and Ruby's thread scheduling don't mix).
|
140
|
+
test "Bubble EAGAIN without retrying" do
|
141
|
+
cmd = %{(sleep 0.3; echo "+PONG\r\n") | nc -l 6380}
|
142
|
+
IO.popen(cmd) do |_|
|
143
|
+
sleep 0.1 # Give nc a little time to start listening
|
144
|
+
redis = Redis.connect(:port => 6380, :timeout => 0.1)
|
145
|
+
|
146
|
+
begin
|
147
|
+
assert_raise(Errno::EAGAIN) { redis.ping }
|
148
|
+
ensure
|
149
|
+
# Explicitly close connection so nc can quit
|
150
|
+
redis.client.disconnect
|
151
|
+
|
152
|
+
# Make the reactor loop do a tick to really close
|
153
|
+
EM::Synchrony.sleep(0) if driver == :synchrony
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
data/test/lint/hashes.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
test "HSET and HGET" do |r|
|
2
|
+
r.hset("foo", "f1", "s1")
|
3
|
+
|
4
|
+
assert "s1" == r.hget("foo", "f1")
|
5
|
+
end
|
6
|
+
|
7
|
+
test "HDEL" do |r|
|
8
|
+
r.hset("foo", "f1", "s1")
|
9
|
+
|
10
|
+
assert "s1" == r.hget("foo", "f1")
|
11
|
+
|
12
|
+
r.hdel("foo", "f1")
|
13
|
+
|
14
|
+
assert nil == r.hget("foo", "f1")
|
15
|
+
end
|
16
|
+
|
17
|
+
test "HEXISTS" do |r|
|
18
|
+
assert false == r.hexists("foo", "f1")
|
19
|
+
|
20
|
+
r.hset("foo", "f1", "s1")
|
21
|
+
|
22
|
+
assert r.hexists("foo", "f1")
|
23
|
+
end
|
24
|
+
|
25
|
+
test "HLEN" do |r|
|
26
|
+
assert 0 == r.hlen("foo")
|
27
|
+
|
28
|
+
r.hset("foo", "f1", "s1")
|
29
|
+
|
30
|
+
assert 1 == r.hlen("foo")
|
31
|
+
|
32
|
+
r.hset("foo", "f2", "s2")
|
33
|
+
|
34
|
+
assert 2 == r.hlen("foo")
|
35
|
+
end
|
36
|
+
|
37
|
+
test "HKEYS" do |r|
|
38
|
+
assert [] == r.hkeys("foo")
|
39
|
+
|
40
|
+
r.hset("foo", "f1", "s1")
|
41
|
+
r.hset("foo", "f2", "s2")
|
42
|
+
|
43
|
+
assert ["f1", "f2"] == r.hkeys("foo")
|
44
|
+
end
|
45
|
+
|
46
|
+
test "HVALS" do |r|
|
47
|
+
assert [] == r.hvals("foo")
|
48
|
+
|
49
|
+
r.hset("foo", "f1", "s1")
|
50
|
+
r.hset("foo", "f2", "s2")
|
51
|
+
|
52
|
+
assert ["s1", "s2"] == r.hvals("foo")
|
53
|
+
end
|
54
|
+
|
55
|
+
test "HGETALL" do |r|
|
56
|
+
assert({} == r.hgetall("foo"))
|
57
|
+
|
58
|
+
r.hset("foo", "f1", "s1")
|
59
|
+
r.hset("foo", "f2", "s2")
|
60
|
+
|
61
|
+
assert({"f1" => "s1", "f2" => "s2"} == r.hgetall("foo"))
|
62
|
+
end
|
63
|
+
|
64
|
+
test "HMSET" do |r|
|
65
|
+
r.hmset("hash", "foo1", "bar1", "foo2", "bar2")
|
66
|
+
|
67
|
+
assert "bar1" == r.hget("hash", "foo1")
|
68
|
+
assert "bar2" == r.hget("hash", "foo2")
|
69
|
+
end
|
70
|
+
|
71
|
+
test "HMSET with invalid arguments" do |r|
|
72
|
+
assert_raise(RuntimeError) do
|
73
|
+
r.hmset("hash", "foo1", "bar1", "foo2", "bar2", "foo3")
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
test "Mapped HMSET" do |r|
|
78
|
+
r.mapped_hmset("foo", :f1 => "s1", :f2 => "s2")
|
79
|
+
|
80
|
+
assert "s1" == r.hget("foo", "f1")
|
81
|
+
assert "s2" == r.hget("foo", "f2")
|
82
|
+
end
|
83
|
+
|
84
|
+
test "HMGET" do |r|
|
85
|
+
r.hset("foo", "f1", "s1")
|
86
|
+
r.hset("foo", "f2", "s2")
|
87
|
+
r.hset("foo", "f3", "s3")
|
88
|
+
|
89
|
+
assert ["s2", "s3"] == r.hmget("foo", "f2", "f3")
|
90
|
+
end
|
91
|
+
|
92
|
+
test "HMGET mapped" do |r|
|
93
|
+
r.hset("foo", "f1", "s1")
|
94
|
+
r.hset("foo", "f2", "s2")
|
95
|
+
r.hset("foo", "f3", "s3")
|
96
|
+
|
97
|
+
assert({"f1" => "s1"} == r.mapped_hmget("foo", "f1"))
|
98
|
+
assert({"f1" => "s1", "f2" => "s2"} == r.mapped_hmget("foo", "f1", "f2"))
|
99
|
+
end
|
100
|
+
|
101
|
+
test "HINCRBY" do |r|
|
102
|
+
r.hincrby("foo", "f1", 1)
|
103
|
+
|
104
|
+
assert "1" == r.hget("foo", "f1")
|
105
|
+
|
106
|
+
r.hincrby("foo", "f1", 2)
|
107
|
+
|
108
|
+
assert "3" == r.hget("foo", "f1")
|
109
|
+
|
110
|
+
r.hincrby("foo", "f1", -1)
|
111
|
+
|
112
|
+
assert "2" == r.hget("foo", "f1")
|
113
|
+
end
|
114
|
+
|
@@ -0,0 +1,41 @@
|
|
1
|
+
test "Logger" do |r, log|
|
2
|
+
r.ping
|
3
|
+
|
4
|
+
assert log.string =~ /Redis >> PING/
|
5
|
+
assert log.string =~ /Redis >> \d+\.\d+ms/
|
6
|
+
end
|
7
|
+
|
8
|
+
test "Logger with pipelining" do |r, log|
|
9
|
+
r.pipelined do
|
10
|
+
r.set "foo", "bar"
|
11
|
+
r.get "foo"
|
12
|
+
end
|
13
|
+
|
14
|
+
assert log.string["SET foo bar"]
|
15
|
+
assert log.string["GET foo"]
|
16
|
+
end if $TEST_PIPELINING
|
17
|
+
|
18
|
+
test "Recovers from failed commands" do |r, _|
|
19
|
+
# See http://github.com/ezmobius/redis-rb/issues#issue/28
|
20
|
+
|
21
|
+
assert_raise(ArgumentError) do
|
22
|
+
r.srem "foo"
|
23
|
+
end
|
24
|
+
|
25
|
+
assert_nothing_raised do
|
26
|
+
r.info
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
test "provides a meaningful inspect" do |r, _|
|
31
|
+
assert "#<Redis client v#{Redis::VERSION} connected to redis://127.0.0.1:6379/15 (Redis v#{r.info["redis_version"]})>" == r.inspect
|
32
|
+
end if $TEST_INSPECT
|
33
|
+
|
34
|
+
test "raises on protocol errors" do
|
35
|
+
redis_mock(:ping => lambda { |*_| "foo" }) do
|
36
|
+
assert_raise(Redis::ProtocolError) do
|
37
|
+
Redis.connect(:port => 6380).ping
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|