redis 2.1.1 → 2.2.0
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 +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
|
+
|