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.
Files changed (61) hide show
  1. data/.gitignore +2 -0
  2. data/.yardopts +3 -0
  3. data/CHANGELOG.md +65 -1
  4. data/README.md +6 -0
  5. data/Rakefile +19 -27
  6. data/lib/redis.rb +737 -170
  7. data/lib/redis/client.rb +82 -67
  8. data/lib/redis/connection/command_helper.rb +15 -16
  9. data/lib/redis/connection/hiredis.rb +6 -3
  10. data/lib/redis/connection/ruby.rb +2 -1
  11. data/lib/redis/connection/synchrony.rb +3 -1
  12. data/lib/redis/distributed.rb +20 -18
  13. data/lib/redis/errors.rb +38 -0
  14. data/lib/redis/hash_ring.rb +2 -2
  15. data/lib/redis/pipeline.rb +91 -19
  16. data/lib/redis/subscribe.rb +1 -16
  17. data/lib/redis/version.rb +1 -1
  18. data/redis.gemspec +30 -11
  19. data/test/command_map_test.rb +29 -0
  20. data/test/commands_on_hashes_test.rb +3 -3
  21. data/test/commands_on_lists_test.rb +1 -1
  22. data/test/commands_on_sets_test.rb +0 -2
  23. data/test/commands_on_sorted_sets_test.rb +8 -9
  24. data/test/commands_on_strings_test.rb +3 -3
  25. data/test/commands_on_value_types_test.rb +0 -1
  26. data/test/connection_handling_test.rb +120 -4
  27. data/test/distributed_commands_on_hashes_test.rb +0 -1
  28. data/test/distributed_commands_on_lists_test.rb +0 -1
  29. data/test/distributed_commands_on_sets_test.rb +0 -1
  30. data/test/distributed_commands_on_sorted_sets_test.rb +19 -0
  31. data/test/distributed_commands_on_strings_test.rb +0 -1
  32. data/test/distributed_commands_on_value_types_test.rb +0 -1
  33. data/test/distributed_connection_handling_test.rb +0 -1
  34. data/test/distributed_key_tags_test.rb +0 -1
  35. data/test/distributed_persistence_control_commands_test.rb +0 -1
  36. data/test/distributed_publish_subscribe_test.rb +1 -2
  37. data/test/distributed_remote_server_control_commands_test.rb +2 -3
  38. data/test/distributed_transactions_test.rb +0 -1
  39. data/test/encoding_test.rb +0 -1
  40. data/test/helper.rb +14 -4
  41. data/test/helper_test.rb +8 -0
  42. data/test/internals_test.rb +25 -33
  43. data/test/lint/hashes.rb +17 -3
  44. data/test/lint/internals.rb +2 -3
  45. data/test/lint/lists.rb +17 -3
  46. data/test/lint/sets.rb +30 -6
  47. data/test/lint/sorted_sets.rb +56 -27
  48. data/test/lint/strings.rb +9 -13
  49. data/test/lint/value_types.rb +12 -15
  50. data/test/persistence_control_commands_test.rb +0 -1
  51. data/test/pipelining_commands_test.rb +69 -6
  52. data/test/publish_subscribe_test.rb +1 -1
  53. data/test/redis_mock.rb +14 -5
  54. data/test/remote_server_control_commands_test.rb +8 -2
  55. data/test/sorting_test.rb +0 -1
  56. data/test/test.conf +1 -0
  57. data/test/transactions_test.rb +88 -15
  58. data/test/unknown_commands_test.rb +1 -2
  59. data/test/url_param_test.rb +0 -1
  60. metadata +68 -16
  61. data/lib/redis/compat.rb +0 -21
@@ -54,7 +54,7 @@ class Redis
54
54
 
55
55
  def iter_nodes(key)
56
56
  return [nil,nil] if @ring.size == 0
57
- node, pos = get_node_pos(key)
57
+ _, pos = get_node_pos(key)
58
58
  @sorted_keys[pos..-1].each do |k|
59
59
  yield @ring[k]
60
60
  end
@@ -98,7 +98,7 @@ class Redis
98
98
  }
99
99
  EOM
100
100
  end
101
- rescue Exception => e
101
+ rescue Exception
102
102
  # Find the closest index in HashRing with value <= the given value
103
103
  def binary_search(ary, value, &block)
104
104
  upper = ary.size - 1
@@ -1,34 +1,106 @@
1
1
  class Redis
2
+ unless defined?(::BasicObject)
3
+ class BasicObject
4
+ instance_methods.each { |meth| undef_method(meth) unless meth =~ /\A(__|instance_eval)/ }
5
+ end
6
+ end
7
+
2
8
  class Pipeline
3
- attr :commands
9
+ attr :futures
4
10
 
5
11
  def initialize
6
- @commands = []
12
+ @without_reconnect = false
13
+ @shutdown = false
14
+ @futures = []
7
15
  end
8
16
 
9
- # Starting with 2.2.1, assume that this method is called with a single
10
- # array argument. Check its size for backwards compat.
11
- def call(*args)
12
- if args.first.is_a?(Array) && args.size == 1
13
- command = args.first
14
- else
15
- command = args
16
- end
17
+ def without_reconnect?
18
+ @without_reconnect
19
+ end
17
20
 
18
- @commands << command
19
- nil
21
+ def shutdown?
22
+ @shutdown
20
23
  end
21
24
 
22
- # Assume that this method is called with a single array argument. No
23
- # backwards compat here, since it was introduced in 2.2.2.
24
- def call_without_reply(command)
25
- @commands.push command
26
- nil
25
+ def call(command, &block)
26
+ # A pipeline that contains a shutdown should not raise ECONNRESET when
27
+ # the connection is gone.
28
+ @shutdown = true if command.first == :shutdown
29
+ future = Future.new(command, block)
30
+ @futures << future
31
+ future
27
32
  end
28
33
 
29
- def call_pipelined(commands, options = {})
30
- @commands.concat commands
34
+ def call_pipeline(pipeline)
35
+ @shutdown = true if pipeline.shutdown?
36
+ @futures.concat(pipeline.futures)
31
37
  nil
32
38
  end
39
+
40
+ def commands
41
+ @futures.map { |f| f._command }
42
+ end
43
+
44
+ def without_reconnect(&block)
45
+ @without_reconnect = true
46
+ yield
47
+ end
48
+
49
+ def finish(replies)
50
+ futures.each_with_index.map do |future, i|
51
+ future._set(replies[i])
52
+ end
53
+ end
54
+
55
+ class Multi < self
56
+ def finish(replies)
57
+ return if replies.last.nil? # The transaction failed because of WATCH.
58
+
59
+ if replies.last.size < futures.size - 2
60
+ # Some command wasn't recognized by Redis.
61
+ raise replies.detect { |r| r.kind_of?(::Exception) }
62
+ end
63
+
64
+ super(replies.last)
65
+ end
66
+
67
+ def commands
68
+ [[:multi]] + super + [[:exec]]
69
+ end
70
+ end
71
+ end
72
+
73
+ class FutureNotReady < RuntimeError
74
+ def initialize
75
+ super("Value will be available once the pipeline executes.")
76
+ end
77
+ end
78
+
79
+ class Future < BasicObject
80
+ FutureNotReady = ::Redis::FutureNotReady.new
81
+
82
+ def initialize(command, transformation)
83
+ @command = command
84
+ @transformation = transformation
85
+ @object = FutureNotReady
86
+ end
87
+
88
+ def inspect
89
+ "<Redis::Future #{@command.inspect}>"
90
+ end
91
+
92
+ def _set(object)
93
+ @object = @transformation ? @transformation.call(object) : object
94
+ value
95
+ end
96
+
97
+ def _command
98
+ @command
99
+ end
100
+
101
+ def value
102
+ ::Kernel.raise(@object) if @object.kind_of?(::Exception)
103
+ @object
104
+ end
33
105
  end
34
106
  end
@@ -4,25 +4,10 @@ class Redis
4
4
  @client = client
5
5
  end
6
6
 
7
- # Starting with 2.2.1, assume that this method is called with a single
8
- # array argument. Check its size for backwards compat.
9
- def call(*args)
10
- if args.first.is_a?(Array) && args.size == 1
11
- command = args.first
12
- else
13
- command = args
14
- end
15
-
7
+ def call(command)
16
8
  @client.process([command])
17
9
  end
18
10
 
19
- # Assume that this method is called with a single array argument. No
20
- # backwards compat here, since it was introduced in 2.2.2.
21
- def call_without_reply(command)
22
- @commands.push command
23
- nil
24
- end
25
-
26
11
  def subscribe(*channels, &block)
27
12
  subscription("subscribe", "unsubscribe", channels, block)
28
13
  end
data/lib/redis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "2.2.2"
2
+ VERSION = "3.0.0.rc1"
3
3
  end
data/redis.gemspec CHANGED
@@ -1,24 +1,43 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
 
3
3
  $:.unshift File.expand_path("../lib", __FILE__)
4
+
4
5
  require "redis/version"
5
6
 
6
7
  Gem::Specification.new do |s|
7
- s.name = %q{redis}
8
+ s.name = "redis"
9
+
8
10
  s.version = Redis::VERSION
9
11
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Ezra Zygmuntowicz", "Taylor Weibley", "Matthew Clark", "Brian McKinney", "Salvatore Sanfilippo", "Luca Guidi", "Michel Martens", "Damian Janowski", "Pieter Noordhuis"]
12
- s.autorequire = %q{redis}
13
- s.description = %q{Ruby client library for Redis, the key value storage server}
14
- s.summary = %q{Ruby client library for Redis, the key value storage server}
15
- s.email = %q{ez@engineyard.com}
16
- s.homepage = %q{http://github.com/ezmobius/redis-rb}
17
- s.rubyforge_project = "redis-rb"
12
+ s.homepage = "https://github.com/ezmobius/redis-rb"
13
+
14
+ s.summary = "A Ruby client library for the Redis key-value store."
15
+
16
+ s.description = <<-EOS
17
+ A simple Ruby client trying to match Redis' API one-to-one while still providing a Rubystic interface.
18
+ It features thread safety, client-side sharding, and an obsession for performance.
19
+ EOS
20
+
21
+ s.authors = [
22
+ "Ezra Zygmuntowicz",
23
+ "Taylor Weibley",
24
+ "Matthew Clark",
25
+ "Brian McKinney",
26
+ "Salvatore Sanfilippo",
27
+ "Luca Guidi",
28
+ "Michel Martens",
29
+ "Damian Janowski",
30
+ "Pieter Noordhuis"
31
+ ]
32
+
33
+ s.email = ["redis-db@googlegroups.com"]
18
34
 
19
35
  s.files = `git ls-files`.split("\n")
20
36
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
21
37
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
22
- s.require_paths = ["lib"]
23
- s.rubygems_version = %q{1.5.0}
38
+
39
+ s.add_development_dependency("rake")
40
+ s.add_development_dependency("cutest")
41
+ s.add_development_dependency("hiredis")
42
+ s.add_development_dependency("em-synchrony")
24
43
  end
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+
3
+ require File.expand_path("./helper", File.dirname(__FILE__))
4
+
5
+ setup do
6
+ init Redis.new(OPTIONS)
7
+ end
8
+
9
+ test "Override existing commands" do |r|
10
+ r.set("counter", 1)
11
+
12
+ assert 2 == r.incr("counter")
13
+
14
+ r.client.command_map[:incr] = :decr
15
+
16
+ assert 1 == r.incr("counter")
17
+ end
18
+
19
+ test "Override non-existing commands" do |r|
20
+ r.set("key", "value")
21
+
22
+ assert_raise Redis::CommandError do
23
+ r.idontexist("key")
24
+ end
25
+
26
+ r.client.command_map[:idontexist] = :get
27
+
28
+ assert "value" == r.idontexist("key")
29
+ end
@@ -8,13 +8,13 @@ end
8
8
 
9
9
  load './test/lint/hashes.rb'
10
10
 
11
- test "Mapped HMGET in a pipeline returns plain array" do |r|
11
+ test "Mapped HMGET in a pipeline returns hash" do |r|
12
12
  r.hset("foo", "f1", "s1")
13
13
  r.hset("foo", "f2", "s2")
14
14
 
15
15
  result = r.pipelined do
16
- assert nil == r.mapped_hmget("foo", "f1", "f2")
16
+ r.mapped_hmget("foo", "f1", "f2")
17
17
  end
18
18
 
19
- assert result[0] == ["s1", "s2"]
19
+ assert result[0] == { "f1" => "s1", "f2" => "s2" }
20
20
  end
@@ -33,7 +33,7 @@ test "LINSERT" do |r|
33
33
 
34
34
  assert ["s1", "s2", "s3"] == r.lrange("foo", 0, -1)
35
35
 
36
- assert_raise(RuntimeError) do
36
+ assert_raise(Redis::CommandError) do
37
37
  r.linsert "foo", :anywhere, "s3", "s2"
38
38
  end
39
39
  end
@@ -74,5 +74,3 @@ test "SDIFFSTORE" do |r|
74
74
 
75
75
  assert ["s1"] == r.smembers("baz")
76
76
  end
77
-
78
-
@@ -79,8 +79,8 @@ test "ZINTERSTORE with WEIGHTS" do |r|
79
79
  assert 2 == r.zinterstore("foobar", ["foo", "bar"], :weights => [10, 1])
80
80
  assert ["s2", "s3"] == r.zrange("foobar", 0, -1)
81
81
 
82
- assert "40" == r.zscore("foobar", "s2")
83
- assert "60" == r.zscore("foobar", "s3")
82
+ assert 40.0 == r.zscore("foobar", "s2")
83
+ assert 60.0 == r.zscore("foobar", "s3")
84
84
  end
85
85
 
86
86
  test "ZINTERSTORE with AGGREGATE" do |r|
@@ -93,17 +93,16 @@ test "ZINTERSTORE with AGGREGATE" do |r|
93
93
 
94
94
  assert 2 == r.zinterstore("foobar", ["foo", "bar"])
95
95
  assert ["s2", "s3"] == r.zrange("foobar", 0, -1)
96
- assert "22" == r.zscore("foobar", "s2")
97
- assert "33" == r.zscore("foobar", "s3")
96
+ assert 22.0 == r.zscore("foobar", "s2")
97
+ assert 33.0 == r.zscore("foobar", "s3")
98
98
 
99
99
  assert 2 == r.zinterstore("foobar", ["foo", "bar"], :aggregate => :min)
100
100
  assert ["s2", "s3"] == r.zrange("foobar", 0, -1)
101
- assert "2" == r.zscore("foobar", "s2")
102
- assert "3" == r.zscore("foobar", "s3")
101
+ assert 2.0 == r.zscore("foobar", "s2")
102
+ assert 3.0 == r.zscore("foobar", "s3")
103
103
 
104
104
  assert 2 == r.zinterstore("foobar", ["foo", "bar"], :aggregate => :max)
105
105
  assert ["s2", "s3"] == r.zrange("foobar", 0, -1)
106
- assert "20" == r.zscore("foobar", "s2")
107
- assert "30" == r.zscore("foobar", "s3")
106
+ assert 20.0 == r.zscore("foobar", "s2")
107
+ assert 30.0 == r.zscore("foobar", "s3")
108
108
  end
109
-
@@ -32,15 +32,15 @@ test "MGET mapped" do |r|
32
32
  assert nil == response["baz"]
33
33
  end
34
34
 
35
- test "Mapped MGET in a pipeline returns plain array" do |r|
35
+ test "Mapped MGET in a pipeline returns hash" do |r|
36
36
  r.set("foo", "s1")
37
37
  r.set("bar", "s2")
38
38
 
39
39
  result = r.pipelined do
40
- assert nil == r.mapped_mget("foo", "bar")
40
+ r.mapped_mget("foo", "bar")
41
41
  end
42
42
 
43
- assert result[0] == ["s1", "s2"]
43
+ assert result[0] == { "foo" => "s1", "bar" => "s2" }
44
44
  end
45
45
 
46
46
  test "MSET" do |r|
@@ -85,4 +85,3 @@ test "FLUSHALL" do
85
85
  assert "FLUSHALL" == redis.flushall
86
86
  end
87
87
  end
88
-
@@ -44,7 +44,11 @@ test "QUIT" do |r|
44
44
  end
45
45
 
46
46
  test "SHUTDOWN" do
47
- redis_mock(:shutdown => lambda { "+SHUTDOWN" }) do
47
+ commands = {
48
+ :shutdown => lambda { :exit }
49
+ }
50
+
51
+ redis_mock(commands) do
48
52
  redis = Redis.new(OPTIONS.merge(:port => 6380))
49
53
 
50
54
  # SHUTDOWN does not reply: test that it does not raise here.
@@ -52,6 +56,117 @@ test "SHUTDOWN" do
52
56
  end
53
57
  end
54
58
 
59
+ test "SHUTDOWN with error" do
60
+ connections = 0
61
+ commands = {
62
+ :select => lambda { |*_| connections += 1; "+OK\r\n" },
63
+ :connections => lambda { ":#{connections}\r\n" },
64
+ :shutdown => lambda { "-ERR could not shutdown\r\n" }
65
+ }
66
+
67
+ redis_mock(commands) do
68
+ redis = Redis.new(OPTIONS.merge(:port => 6380))
69
+
70
+ connections = redis.connections
71
+
72
+ # SHUTDOWN replies with an error: test that it gets raised
73
+ assert_raise Redis::CommandError do
74
+ redis.shutdown
75
+ end
76
+
77
+ # The connection should remain in tact
78
+ assert connections == redis.connections
79
+ end
80
+ end
81
+
82
+ test "SHUTDOWN from pipeline" do
83
+ commands = {
84
+ :shutdown => lambda { :exit }
85
+ }
86
+
87
+ redis_mock(commands) do
88
+ redis = Redis.new(OPTIONS.merge(:port => 6380))
89
+
90
+ result = redis.pipelined do
91
+ redis.shutdown
92
+ end
93
+
94
+ assert nil == result
95
+ assert !redis.client.connected?
96
+ end
97
+ end
98
+
99
+ test "SHUTDOWN with error from pipeline" do
100
+ connections = 0
101
+ commands = {
102
+ :select => lambda { |*_| connections += 1; "+OK\r\n" },
103
+ :connections => lambda { ":#{connections}\r\n" },
104
+ :shutdown => lambda { "-ERR could not shutdown\r\n" }
105
+ }
106
+
107
+ redis_mock(commands) do
108
+ redis = Redis.new(OPTIONS.merge(:port => 6380))
109
+
110
+ connections = redis.connections
111
+
112
+ # SHUTDOWN replies with an error: test that it gets raised
113
+ assert_raise Redis::CommandError do
114
+ redis.pipelined do
115
+ redis.shutdown
116
+ end
117
+ end
118
+
119
+ # The connection should remain in tact
120
+ assert connections == redis.connections
121
+ end
122
+ end
123
+
124
+ test "SHUTDOWN from MULTI/EXEC" do
125
+ commands = {
126
+ :multi => lambda { "+OK\r\n" },
127
+ :shutdown => lambda { "+QUEUED\r\n" },
128
+ :exec => lambda { :exit }
129
+ }
130
+
131
+ redis_mock(commands) do
132
+ redis = Redis.new(OPTIONS.merge(:port => 6380))
133
+
134
+ result = redis.multi do
135
+ redis.shutdown
136
+ end
137
+
138
+ assert nil == result
139
+ assert !redis.client.connected?
140
+ end
141
+ end
142
+
143
+ test "SHUTDOWN with error from MULTI/EXEC" do
144
+ connections = 0
145
+ commands = {
146
+ :select => lambda { |*_| connections += 1; "+OK\r\n" },
147
+ :connections => lambda { ":#{connections}\r\n" },
148
+ :multi => lambda { "+OK\r\n" },
149
+ :shutdown => lambda { "+QUEUED\r\n" },
150
+ :exec => lambda { "*1\r\n-ERR could not shutdown\r\n" }
151
+ }
152
+
153
+ redis_mock(commands) do
154
+ redis = Redis.new(OPTIONS.merge(:port => 6380))
155
+
156
+ connections = redis.connections
157
+
158
+ # SHUTDOWN replies with an error: test that it gets returned
159
+ # We should test for Redis::CommandError here, but hiredis doesn't yet do
160
+ # custom error classes.
161
+ assert_raise(StandardError) do
162
+ redis.multi { redis.shutdown }
163
+ end
164
+
165
+ # The connection should remain intact
166
+ assert connections == redis.connections
167
+ end
168
+ end
169
+
55
170
  test "SLAVEOF" do
56
171
  redis_mock(:slaveof => lambda { |host, port| "+SLAVEOF #{host} #{port}" }) do
57
172
  redis = Redis.new(OPTIONS.merge(:port => 6380))
@@ -69,9 +184,11 @@ test "BGREWRITEAOF" do
69
184
  end
70
185
 
71
186
  test "CONFIG GET" do |r|
72
- assert "300" == r.config(:get, "*")["timeout"]
187
+ assert r.config(:get, "*")["timeout"] != nil
73
188
 
74
- assert r.config(:get, "timeout") == { "timeout" => "300" }
189
+ config = r.config(:get, "timeout")
190
+ assert ["timeout"] == config.keys
191
+ assert config.values.compact.size > 0
75
192
  end
76
193
 
77
194
  test "CONFIG SET" do |r|
@@ -85,4 +202,3 @@ test "CONFIG SET" do |r|
85
202
  r.config :set, "timeout", 300
86
203
  end
87
204
  end
88
-