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