redis 2.1.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. data/.gitignore +8 -0
  2. data/CHANGELOG.md +34 -0
  3. data/README.md +190 -0
  4. data/Rakefile +194 -79
  5. data/benchmarking/logging.rb +62 -0
  6. data/benchmarking/pipeline.rb +51 -0
  7. data/benchmarking/speed.rb +21 -0
  8. data/benchmarking/suite.rb +24 -0
  9. data/benchmarking/thread_safety.rb +38 -0
  10. data/benchmarking/worker.rb +71 -0
  11. data/examples/basic.rb +15 -0
  12. data/examples/dist_redis.rb +43 -0
  13. data/examples/incr-decr.rb +17 -0
  14. data/examples/list.rb +26 -0
  15. data/examples/pubsub.rb +31 -0
  16. data/examples/sets.rb +36 -0
  17. data/examples/unicorn/config.ru +3 -0
  18. data/examples/unicorn/unicorn.rb +20 -0
  19. data/lib/redis.rb +612 -156
  20. data/lib/redis/client.rb +98 -57
  21. data/lib/redis/connection.rb +9 -134
  22. data/lib/redis/connection/command_helper.rb +45 -0
  23. data/lib/redis/connection/hiredis.rb +49 -0
  24. data/lib/redis/connection/registry.rb +12 -0
  25. data/lib/redis/connection/ruby.rb +131 -0
  26. data/lib/redis/connection/synchrony.rb +125 -0
  27. data/lib/redis/distributed.rb +161 -5
  28. data/lib/redis/pipeline.rb +6 -0
  29. data/lib/redis/version.rb +3 -0
  30. data/redis.gemspec +24 -0
  31. data/test/commands_on_hashes_test.rb +32 -0
  32. data/test/commands_on_lists_test.rb +60 -0
  33. data/test/commands_on_sets_test.rb +78 -0
  34. data/test/commands_on_sorted_sets_test.rb +109 -0
  35. data/test/commands_on_strings_test.rb +80 -0
  36. data/test/commands_on_value_types_test.rb +88 -0
  37. data/test/connection_handling_test.rb +87 -0
  38. data/test/db/.gitignore +1 -0
  39. data/test/distributed_blocking_commands_test.rb +53 -0
  40. data/test/distributed_commands_on_hashes_test.rb +12 -0
  41. data/test/distributed_commands_on_lists_test.rb +24 -0
  42. data/test/distributed_commands_on_sets_test.rb +85 -0
  43. data/test/distributed_commands_on_strings_test.rb +50 -0
  44. data/test/distributed_commands_on_value_types_test.rb +73 -0
  45. data/test/distributed_commands_requiring_clustering_test.rb +148 -0
  46. data/test/distributed_connection_handling_test.rb +25 -0
  47. data/test/distributed_internals_test.rb +18 -0
  48. data/test/distributed_key_tags_test.rb +53 -0
  49. data/test/distributed_persistence_control_commands_test.rb +24 -0
  50. data/test/distributed_publish_subscribe_test.rb +101 -0
  51. data/test/distributed_remote_server_control_commands_test.rb +31 -0
  52. data/test/distributed_sorting_test.rb +21 -0
  53. data/test/distributed_test.rb +60 -0
  54. data/test/distributed_transactions_test.rb +34 -0
  55. data/test/encoding_test.rb +16 -0
  56. data/test/error_replies_test.rb +53 -0
  57. data/test/helper.rb +145 -0
  58. data/test/internals_test.rb +157 -0
  59. data/test/lint/hashes.rb +114 -0
  60. data/test/lint/internals.rb +41 -0
  61. data/test/lint/lists.rb +93 -0
  62. data/test/lint/sets.rb +66 -0
  63. data/test/lint/sorted_sets.rb +167 -0
  64. data/test/lint/strings.rb +137 -0
  65. data/test/lint/value_types.rb +84 -0
  66. data/test/persistence_control_commands_test.rb +22 -0
  67. data/test/pipelining_commands_test.rb +123 -0
  68. data/test/publish_subscribe_test.rb +158 -0
  69. data/test/redis_mock.rb +80 -0
  70. data/test/remote_server_control_commands_test.rb +63 -0
  71. data/test/sorting_test.rb +44 -0
  72. data/test/synchrony_driver.rb +57 -0
  73. data/test/test.conf +8 -0
  74. data/test/thread_safety_test.rb +30 -0
  75. data/test/transactions_test.rb +100 -0
  76. data/test/unknown_commands_test.rb +14 -0
  77. data/test/url_param_test.rb +60 -0
  78. metadata +128 -19
  79. data/README.markdown +0 -129
@@ -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
- @host = options[:host] || "127.0.0.1"
9
- @port = (options[:port] || 6379).to_i
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).to_i
15
+ @timeout = (options[:timeout] || 5).to_f
12
16
  @password = options[:password]
13
17
  @logger = options[:logger]
14
- @connection = Connection.new
18
+ @reconnect = true
19
+ @connection = Connection.drivers.last.new
15
20
  end
16
21
 
17
22
  def connect
18
- connect_to(@host, @port)
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://#{host}:#{port}/#{db}"
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) do
30
- read
31
- end
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
- without_socket_timeout do
44
+ error = nil
45
+
46
+ result = without_socket_timeout do
36
47
  process(args) do
37
- loop { yield(read) }
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
- process(*commands) do
44
- Array.new(commands.size) { read }
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 connect_to(host, port)
133
- with_timeout(@timeout) do
134
- connection.connect(host, port)
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 #{host}:#{port}"
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
- connect unless connected?
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
- if reconnect
157
- yield
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
@@ -1,134 +1,9 @@
1
- class Redis
2
- class Connection
3
- MINUS = "-".freeze
4
- PLUS = "+".freeze
5
- COLON = ":".freeze
6
- DOLLAR = "$".freeze
7
- ASTERISK = "*".freeze
8
-
9
- def initialize
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