redis 0.2.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,36 +1,74 @@
1
1
  # redis-rb
2
2
 
3
- A ruby client library for the redis key value storage system.
3
+ A Ruby client library for the [Redis](http://code.google.com/p/redis) key-value storage system.
4
4
 
5
- ## Information about redis
5
+ ## Information about Redis
6
6
 
7
- Redis is a key value store with some interesting features:
8
- 1. It's fast.
9
- 2. Keys are strings but values can have types of "NONE", "STRING", "LIST", or "SET". List's can be atomically push'd, pop'd, lpush'd, lpop'd and indexed. This allows you to store things like lists of comments under one key while retaining the ability to append comments without reading and putting back the whole list.
10
-
11
- See [redis on code.google.com](http://code.google.com/p/redis/wiki/README) for more information.
12
-
13
- See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb)
7
+ Redis is a key-value store with some interesting features:
14
8
 
15
- ## Dependencies
16
-
17
- 1. rspec -
18
- sudo gem install rspec
19
-
20
- 2. redis -
9
+ 1. It's fast.
10
+ 2. Keys are strings but values are typed. Currently Redis supports strings, lists, sets, sorted sets and hashes. [Atomic operations](http://code.google.com/p/redis/wiki/CommandReference) can be done on all of these types.
11
+
12
+ See [the Redis homepage](http://code.google.com/p/redis/wiki/README) for more information.
13
+
14
+ ## Usage
15
+
16
+ For all types redis-rb needs redis-server running to connect to.
17
+
18
+ ### Simple Key Value Strings can be used like a large Ruby Hash (Similar to Memcached, Tokyo Cabinet)
19
+
20
+ require 'redis'
21
+ r = Redis.new
22
+ r['key_one'] = "value_one"
23
+ r['key_two'] = "value_two"
24
+
25
+ r['key_one] # => "value_one"
26
+
27
+ ### Redis only stores strings. To store Objects, Array or Hashes, you must [Marshal](http://ruby-doc.org/core/classes/Marshal.html)
28
+
29
+ require 'redis'
30
+ r = Redis.new
31
+
32
+ example_hash_to_store = {:name => "Alex", :age => 21, :password => "letmein", :cool => false}
33
+
34
+ r['key_one'] = Marshal.dump(example_hash_to_store)
35
+
36
+ hash_returned_from_redis = Marshal.load(r['key_one'])
37
+
38
+ ### Alternatively you can use the [Redis Commands](http://code.google.com/p/redis/wiki/CommandReference)
39
+
40
+ require 'redis'
41
+ r = Redis.new
42
+ r.set 'key_one', 'value_one'
43
+ r.get 'key_one' # => 'value_one'
44
+
45
+ # Using Redis list objects
46
+ # Push an object to the head of the list. Creates the list if it doesn't allready exsist.
47
+
48
+ blog_hash = {:title => "Redis Rules!", :body => "Ok so, like why, well like, RDBMS is like....", :created_at => Time.now.to_i}
49
+ r.lpush 'all_blogs', Marshal.dump(blog_hash)
50
+
51
+ # Get a range of strings from the all_blogs list. Similar to offset and limit in SQL (-1, means the last one)
52
+
53
+ r.lrange 'all_blogs', 0, -1
54
+
55
+ ### Multiple commands at once!
56
+
57
+ require 'redis'
58
+ r = Redis.new
59
+ r.multi do
60
+ r.set 'foo', 'bar'
61
+ r.incr 'baz'
62
+ end
63
+
64
+ ## Contributing
65
+
66
+ See the build on [RunCodeRun](http://runcoderun.com/rsanheim/redis-rb).
67
+
68
+ If you would like to submit patches, you'll need Redis in your development environment:
21
69
 
22
70
  rake redis:install
23
71
 
24
- 2. dtach -
25
-
26
- rake dtach:install
27
-
28
- 3. git - git is the new black.
29
-
30
- ## Setup
31
-
32
- Use the tasks mentioned above (in Dependencies) to get your machine setup.
33
-
34
72
  ## Examples
35
73
 
36
- Check the examples/ directory. *Note* you need to have redis-server running first.
74
+ Check the `examples/` directory. You'll need to have an instance of `redis-server` running before running the examples.
data/Rakefile CHANGED
@@ -1,8 +1,6 @@
1
1
  require 'rubygems'
2
2
  require 'rake/gempackagetask'
3
- require 'rubygems/specification'
4
- require 'date'
5
- require 'spec/rake/spectask'
3
+ require 'rake/testtask'
6
4
  require 'tasks/redis.tasks'
7
5
 
8
6
  $:.unshift File.join(File.dirname(__FILE__), 'lib')
@@ -10,7 +8,7 @@ require 'redis'
10
8
 
11
9
  GEM = 'redis'
12
10
  GEM_NAME = 'redis'
13
- GEM_VERSION = RedisRb::VERSION
11
+ GEM_VERSION = Redis::VERSION
14
12
  AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
15
13
  EMAIL = "ez@engineyard.com"
16
14
  HOMEPAGE = "http://github.com/ezmobius/redis-rb"
@@ -33,12 +31,10 @@ spec = Gem::Specification.new do |s|
33
31
  s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
34
32
  end
35
33
 
36
- task :default => :spec
34
+ task :default => :test
37
35
 
38
- desc "Run specs"
39
- Spec::Rake::SpecTask.new do |t|
40
- t.spec_files = FileList['spec/**/*_spec.rb']
41
- t.spec_opts = %w(-fs --color)
36
+ Rake::TestTask.new(:test) do |t|
37
+ t.pattern = 'test/**/*_test.rb'
42
38
  end
43
39
 
44
40
  Rake::GemPackageTask.new(spec) do |pkg|
@@ -51,14 +47,8 @@ task :install => [:package] do
51
47
  end
52
48
 
53
49
  desc "create a gemspec file"
54
- task :make_spec do
50
+ task :gemspec do
55
51
  File.open("#{GEM}.gemspec", "w") do |file|
56
52
  file.puts spec.to_ruby
57
53
  end
58
54
  end
59
-
60
- desc "Run all examples with RCov"
61
- Spec::Rake::SpecTask.new(:rcov) do |t|
62
- t.spec_files = FileList['spec/**/*_spec.rb']
63
- t.rcov = true
64
- end
@@ -1,23 +1,24 @@
1
1
  require 'socket'
2
2
 
3
- module RedisRb
4
- VERSION = "0.2.0"
3
+ class Redis
4
+ VERSION = "1.0.0"
5
+
6
+ def self.new(*attrs)
7
+ Client.new(*attrs)
8
+ end
5
9
  end
6
10
 
7
11
  begin
8
12
  if RUBY_VERSION >= '1.9'
9
13
  require 'timeout'
10
- RedisRb::RedisTimer = Timeout
14
+ Redis::Timer = Timeout
11
15
  else
12
16
  require 'system_timer'
13
- RedisRb::RedisTimer = SystemTimer
17
+ Redis::Timer = SystemTimer
14
18
  end
15
19
  rescue LoadError
16
- RedisRb::RedisTimer = nil
20
+ Redis::Timer = nil
17
21
  end
18
22
 
19
23
  require 'redis/client'
20
24
  require 'redis/pipeline'
21
-
22
- # For backwards compatibility
23
- Redis = RedisRb::Client
@@ -1,4 +1,4 @@
1
- module RedisRb
1
+ class Redis
2
2
  class Client
3
3
  OK = "OK".freeze
4
4
  MINUS = "-".freeze
@@ -28,7 +28,8 @@ module RedisRb
28
28
  "zrevrank" => true,
29
29
  "hget" => true,
30
30
  "hdel" => true,
31
- "hexists" => true
31
+ "hexists" => true,
32
+ "publish" => true
32
33
  }
33
34
 
34
35
  MULTI_BULK_COMMANDS = {
@@ -134,29 +135,195 @@ module RedisRb
134
135
  @binary_keys = options[:binary_keys]
135
136
  @mutex = Mutex.new if @thread_safe
136
137
  @sock = nil
138
+ @pubsub = false
137
139
 
138
- @logger.info { self.to_s } if @logger
140
+ log(self)
139
141
  end
140
142
 
141
143
  def to_s
142
144
  "Redis Client connected to #{server} against DB #{@db}"
143
145
  end
144
146
 
145
- def server
146
- "#{@host}:#{@port}"
147
+ def select(*args)
148
+ raise "SELECT not allowed, use the :db option when creating the object"
147
149
  end
148
150
 
149
- def connect_to_server
150
- @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
151
- call_command(["auth",@password]) if @password
152
- call_command(["select",@db]) unless @db == 0
151
+ def [](key)
152
+ get(key)
153
+ end
154
+
155
+ def []=(key,value)
156
+ set(key, value)
157
+ end
158
+
159
+ def set(key, value, ttl = nil)
160
+ if ttl
161
+ deprecated("set with an expire", :set_with_expire, caller[0])
162
+ set_with_expire(key, value, ttl)
163
+ else
164
+ call_command([:set, key, value])
165
+ end
166
+ end
167
+
168
+ def set_with_expire(key, value, ttl)
169
+ multi do
170
+ set(key, value)
171
+ expire(key, ttl)
172
+ end
173
+ end
174
+
175
+ def mset(*args)
176
+ if args.size == 1
177
+ deprecated("mset with a hash", :mapped_mset, caller[0])
178
+ mapped_mset(args[0])
179
+ else
180
+ call_command(args.unshift(:mset))
181
+ end
182
+ end
183
+
184
+ def mapped_mset(hash)
185
+ mset(*hash.to_a.flatten)
186
+ end
187
+
188
+ def msetnx(*args)
189
+ if args.size == 1
190
+ deprecated("msetnx with a hash", :mapped_msetnx, caller[0])
191
+ mapped_msetnx(args[0])
192
+ else
193
+ call_command(args.unshift(:msetnx))
194
+ end
195
+ end
196
+
197
+ def mapped_msetnx(hash)
198
+ msetnx(*hash.to_a.flatten)
199
+ end
200
+
201
+ # Similar to memcache.rb's #get_multi, returns a hash mapping
202
+ # keys to values.
203
+ def mapped_mget(*keys)
204
+ result = {}
205
+ mget(*keys).each do |value|
206
+ key = keys.shift
207
+ result.merge!(key => value) unless value.nil?
208
+ end
209
+ result
210
+ end
211
+
212
+ def sort(key, options = {})
213
+ cmd = ["SORT"]
214
+ cmd << key
215
+ cmd << "BY #{options[:by]}" if options[:by]
216
+ cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
217
+ cmd << "#{options[:order]}" if options[:order]
218
+ cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
219
+ cmd << "STORE #{options[:store]}" if options[:store]
220
+ call_command(cmd)
221
+ end
222
+
223
+ def incr(key, increment = nil)
224
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
225
+ end
226
+
227
+ def decr(key,decrement = nil)
228
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
229
+ end
230
+
231
+ # Ruby defines a now deprecated type method so we need to override it here
232
+ # since it will never hit method_missing
233
+ def type(key)
234
+ call_command(['type', key])
235
+ end
236
+
237
+ def quit
238
+ call_command(['quit'])
239
+ rescue Errno::ECONNRESET
240
+ end
241
+
242
+ def pipelined(&block)
243
+ pipeline = Pipeline.new self
244
+ yield pipeline
245
+ pipeline.execute
246
+ end
247
+
248
+ def exec
249
+ # Need to override Kernel#exec.
250
+ call_command([:exec])
251
+ end
252
+
253
+ def multi(&block)
254
+ result = call_command [:multi]
255
+
256
+ return result unless block_given?
257
+
258
+ begin
259
+ yield(self)
260
+ exec
261
+ rescue Exception => e
262
+ discard
263
+ raise e
264
+ end
265
+ end
266
+
267
+ def subscribe(*classes)
268
+ # Sanity check, as our API is a bit tricky. You MUST call
269
+ # the top-level subscribe with a block, but you can NOT call
270
+ # the nested subscribe calls with a block, as all the messages
271
+ # are processed by the top level call.
272
+ if @pubsub == false and !block_given?
273
+ raise "You must pass a block to the top level subscribe call"
274
+ elsif @pubsub == true and block_given?
275
+ raise "Can't pass a block to nested subscribe calls"
276
+ elsif @pubsub == true
277
+ # This is a nested subscribe call without a block given.
278
+ # We just need to send the subscribe command and return asap.
279
+ call_command [:subscribe,*classes]
280
+ return true
281
+ end
282
+ @pubsub = true
283
+ call_command [:subscribe,*classes]
284
+ while true
285
+ r = read_reply
286
+ msg = {:type => r[0], :class => r[1], :data => r[2]}
287
+ yield(msg)
288
+ break if msg[:type] == "unsubscribe" and r[2] == 0
289
+ end
290
+ @pubsub = false
291
+ end
292
+
293
+ def unsubscribe(*classes)
294
+ call_command [:unsubscribe,*classes]
295
+ return true
296
+ end
297
+
298
+ protected
299
+
300
+ def call_command(argv)
301
+ log(argv.inspect, :debug)
302
+
303
+ # this wrapper to raw_call_command handle reconnection on socket
304
+ # error. We try to reconnect just one time, otherwise let the error
305
+ # araise.
306
+ connect_to_server if !@sock
307
+
308
+ begin
309
+ raw_call_command(argv.dup)
310
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
311
+ @sock.close rescue nil
312
+ @sock = nil
313
+ connect_to_server
314
+ raw_call_command(argv.dup)
315
+ end
316
+ end
317
+
318
+ def server
319
+ "#{@host}:#{@port}"
153
320
  end
154
321
 
155
322
  def connect_to(host, port, timeout=nil)
156
323
  # We support connect() timeout only if system_timer is availabe
157
324
  # or if we are running against Ruby >= 1.9
158
325
  # Timeout reading from the socket instead will be supported anyway.
159
- if @timeout != 0 and RedisTimer
326
+ if @timeout != 0 and Timer
160
327
  begin
161
328
  sock = TCPSocket.new(host, port)
162
329
  rescue Timeout::Error
@@ -180,32 +347,20 @@ module RedisRb
180
347
  sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
181
348
  rescue Exception => ex
182
349
  # Solaris, for one, does not like/support socket timeouts.
183
- @logger.info "Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}" if @logger
350
+ log("Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}")
184
351
  end
185
352
  end
186
353
  sock
187
354
  end
188
355
 
189
- def method_missing(*argv)
190
- call_command(argv)
356
+ def connect_to_server
357
+ @sock = connect_to(@host, @port, @timeout == 0 ? nil : @timeout)
358
+ call_command(["auth",@password]) if @password
359
+ call_command(["select",@db]) unless @db == 0
191
360
  end
192
361
 
193
- def call_command(argv)
194
- @logger.debug { argv.inspect } if @logger
195
-
196
- # this wrapper to raw_call_command handle reconnection on socket
197
- # error. We try to reconnect just one time, otherwise let the error
198
- # araise.
199
- connect_to_server if !@sock
200
-
201
- begin
202
- raw_call_command(argv.dup)
203
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
204
- @sock.close rescue nil
205
- @sock = nil
206
- connect_to_server
207
- raw_call_command(argv.dup)
208
- end
362
+ def method_missing(*argv)
363
+ call_command(argv)
209
364
  end
210
365
 
211
366
  def raw_call_command(argvp)
@@ -231,8 +386,11 @@ module RedisRb
231
386
  command = ""
232
387
  argvv.each do |argv|
233
388
  bulk = nil
234
- argv[0] = argv[0].to_s.downcase
235
- argv[0] = ALIASES[argv[0]] if ALIASES[argv[0]]
389
+ argv[0] = argv[0].to_s
390
+ if ALIASES[argv[0]]
391
+ deprecated(argv[0], ALIASES[argv[0]], caller[4])
392
+ argv[0] = ALIASES[argv[0]]
393
+ end
236
394
  raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
237
395
  if BULK_COMMANDS[argv[0]] and argv.length > 1
238
396
  bulk = argv[-1].to_s
@@ -242,8 +400,13 @@ module RedisRb
242
400
  command << "#{bulk}\r\n" if bulk
243
401
  end
244
402
  end
403
+ # When in Pub/Sub mode we don't read replies synchronously.
404
+ if @pubsub
405
+ @sock.write(command)
406
+ return true
407
+ end
408
+ # The normal command execution is reading and processing the reply.
245
409
  results = maybe_lock { process_command(command, argvv) }
246
-
247
410
  return pipeline ? results : results[0]
248
411
  end
249
412
 
@@ -263,89 +426,6 @@ module RedisRb
263
426
  end
264
427
  end
265
428
 
266
- def select(*args)
267
- raise "SELECT not allowed, use the :db option when creating the object"
268
- end
269
-
270
- def [](key)
271
- self.get(key)
272
- end
273
-
274
- def []=(key,value)
275
- set(key,value)
276
- end
277
-
278
- def set(key, value, expiry=nil)
279
- s = call_command([:set, key, value]) == OK
280
- expire(key, expiry) if s && expiry
281
- s
282
- end
283
-
284
- def mset(*args)
285
- hsh = args.pop if Hash === args.last
286
- if hsh
287
- call_command(hsh.to_a.flatten.unshift(:mset))
288
- else
289
- call_command(args.unshift(:mset))
290
- end
291
- end
292
-
293
- def msetnx(*args)
294
- hsh = args.pop if Hash === args.last
295
- if hsh
296
- call_command(hsh.to_a.flatten.unshift(:msetnx))
297
- else
298
- call_command(args.unshift(:msetnx))
299
- end
300
- end
301
-
302
- def sort(key, options = {})
303
- cmd = ["SORT"]
304
- cmd << key
305
- cmd << "BY #{options[:by]}" if options[:by]
306
- cmd << "GET #{[options[:get]].flatten * ' GET '}" if options[:get]
307
- cmd << "#{options[:order]}" if options[:order]
308
- cmd << "LIMIT #{options[:limit].join(' ')}" if options[:limit]
309
- cmd << "STORE #{options[:store]}" if options[:store]
310
- call_command(cmd)
311
- end
312
-
313
- def incr(key, increment = nil)
314
- call_command(increment ? ["incrby",key,increment] : ["incr",key])
315
- end
316
-
317
- def decr(key,decrement = nil)
318
- call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
319
- end
320
-
321
- # Similar to memcache.rb's #get_multi, returns a hash mapping
322
- # keys to values.
323
- def mapped_mget(*keys)
324
- result = {}
325
- mget(*keys).each do |value|
326
- key = keys.shift
327
- result.merge!(key => value) unless value.nil?
328
- end
329
- result
330
- end
331
-
332
- # Ruby defines a now deprecated type method so we need to override it here
333
- # since it will never hit method_missing
334
- def type(key)
335
- call_command(['type', key])
336
- end
337
-
338
- def quit
339
- call_command(['quit'])
340
- rescue Errno::ECONNRESET
341
- end
342
-
343
- def pipelined(&block)
344
- pipeline = Pipeline.new self
345
- yield pipeline
346
- pipeline.execute
347
- end
348
-
349
429
  def read_reply
350
430
  # We read the first byte using read() mainly because gets() is
351
431
  # immune to raw socket timeouts.
@@ -387,28 +467,16 @@ module RedisRb
387
467
  end
388
468
  end
389
469
 
390
- def exec
391
- # Need to override Kernel#exec.
392
- call_command([:exec])
470
+ def get_size(string)
471
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
393
472
  end
394
473
 
395
- def multi(&block)
396
- result = call_command [:multi]
397
-
398
- return result unless block_given?
399
-
400
- begin
401
- yield(self)
402
- exec
403
- rescue Exception => e
404
- discard
405
- raise e
406
- end
474
+ def log(str, level = :info)
475
+ @logger.send(level, str.to_s) if @logger
407
476
  end
408
477
 
409
- private
410
- def get_size(string)
411
- string.respond_to?(:bytesize) ? string.bytesize : string.size
412
- end
478
+ def deprecated(old, new, trace = caller[0])
479
+ $stderr.puts "\nRedis: The method #{old} is deprecated. Use #{new} instead (in #{trace})"
480
+ end
413
481
  end
414
482
  end