redis 0.1.2 → 1.0.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/README.markdown CHANGED
@@ -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,14 +1,14 @@
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
 
6
+ $:.unshift File.join(File.dirname(__FILE__), 'lib')
7
+ require 'redis'
8
8
 
9
9
  GEM = 'redis'
10
10
  GEM_NAME = 'redis'
11
- GEM_VERSION = '0.1.2'
11
+ GEM_VERSION = Redis::VERSION
12
12
  AUTHORS = ['Ezra Zygmuntowicz', 'Taylor Weibley', 'Matthew Clark', 'Brian McKinney', 'Salvatore Sanfilippo', 'Luca Guidi']
13
13
  EMAIL = "ez@engineyard.com"
14
14
  HOMEPAGE = "http://github.com/ezmobius/redis-rb"
@@ -31,12 +31,10 @@ spec = Gem::Specification.new do |s|
31
31
  s.files = %w(LICENSE README.markdown Rakefile) + Dir.glob("{lib,tasks,spec}/**/*")
32
32
  end
33
33
 
34
- task :default => :spec
34
+ task :default => :test
35
35
 
36
- desc "Run specs"
37
- Spec::Rake::SpecTask.new do |t|
38
- t.spec_files = FileList['spec/**/*_spec.rb']
39
- t.spec_opts = %w(-fs --color)
36
+ Rake::TestTask.new(:test) do |t|
37
+ t.pattern = 'test/**/*_test.rb'
40
38
  end
41
39
 
42
40
  Rake::GemPackageTask.new(spec) do |pkg|
@@ -49,14 +47,8 @@ task :install => [:package] do
49
47
  end
50
48
 
51
49
  desc "create a gemspec file"
52
- task :make_spec do
50
+ task :gemspec do
53
51
  File.open("#{GEM}.gemspec", "w") do |file|
54
52
  file.puts spec.to_ruby
55
53
  end
56
54
  end
57
-
58
- desc "Run all examples with RCov"
59
- Spec::Rake::SpecTask.new(:rcov) do |t|
60
- t.spec_files = FileList['spec/**/*_spec.rb']
61
- t.rcov = true
62
- end
data/lib/edis.rb ADDED
@@ -0,0 +1,3 @@
1
+ # This file allows for the running of redis-rb with a nice
2
+ # command line look-and-feel: irb -rubygems -redis foo.rb
3
+ require 'redis'
@@ -0,0 +1,482 @@
1
+ class Redis
2
+ class Client
3
+ OK = "OK".freeze
4
+ MINUS = "-".freeze
5
+ PLUS = "+".freeze
6
+ COLON = ":".freeze
7
+ DOLLAR = "$".freeze
8
+ ASTERISK = "*".freeze
9
+
10
+ BULK_COMMANDS = {
11
+ "set" => true,
12
+ "setnx" => true,
13
+ "rpush" => true,
14
+ "lpush" => true,
15
+ "lset" => true,
16
+ "lrem" => true,
17
+ "sadd" => true,
18
+ "srem" => true,
19
+ "sismember" => true,
20
+ "echo" => true,
21
+ "getset" => true,
22
+ "smove" => true,
23
+ "zadd" => true,
24
+ "zincrby" => true,
25
+ "zrem" => true,
26
+ "zscore" => true,
27
+ "zrank" => true,
28
+ "zrevrank" => true,
29
+ "hget" => true,
30
+ "hdel" => true,
31
+ "hexists" => true,
32
+ "publish" => true
33
+ }
34
+
35
+ MULTI_BULK_COMMANDS = {
36
+ "mset" => true,
37
+ "msetnx" => true,
38
+ "hset" => true
39
+ }
40
+
41
+ BOOLEAN_PROCESSOR = lambda{|r| r == 1 }
42
+
43
+ REPLY_PROCESSOR = {
44
+ "exists" => BOOLEAN_PROCESSOR,
45
+ "sismember" => BOOLEAN_PROCESSOR,
46
+ "sadd" => BOOLEAN_PROCESSOR,
47
+ "srem" => BOOLEAN_PROCESSOR,
48
+ "smove" => BOOLEAN_PROCESSOR,
49
+ "zadd" => BOOLEAN_PROCESSOR,
50
+ "zrem" => BOOLEAN_PROCESSOR,
51
+ "move" => BOOLEAN_PROCESSOR,
52
+ "setnx" => BOOLEAN_PROCESSOR,
53
+ "del" => BOOLEAN_PROCESSOR,
54
+ "renamenx" => BOOLEAN_PROCESSOR,
55
+ "expire" => BOOLEAN_PROCESSOR,
56
+ "hset" => BOOLEAN_PROCESSOR,
57
+ "hexists" => BOOLEAN_PROCESSOR,
58
+ "info" => lambda{|r|
59
+ info = {}
60
+ r.each_line {|kv|
61
+ k,v = kv.split(":",2).map{|x| x.chomp}
62
+ info[k.to_sym] = v
63
+ }
64
+ info
65
+ },
66
+ "keys" => lambda{|r|
67
+ if r.is_a?(Array)
68
+ r
69
+ else
70
+ r.split(" ")
71
+ end
72
+ },
73
+ "hgetall" => lambda{|r|
74
+ Hash[*r]
75
+ }
76
+ }
77
+
78
+ ALIASES = {
79
+ "flush_db" => "flushdb",
80
+ "flush_all" => "flushall",
81
+ "last_save" => "lastsave",
82
+ "key?" => "exists",
83
+ "delete" => "del",
84
+ "randkey" => "randomkey",
85
+ "list_length" => "llen",
86
+ "push_tail" => "rpush",
87
+ "push_head" => "lpush",
88
+ "pop_tail" => "rpop",
89
+ "pop_head" => "lpop",
90
+ "list_set" => "lset",
91
+ "list_range" => "lrange",
92
+ "list_trim" => "ltrim",
93
+ "list_index" => "lindex",
94
+ "list_rm" => "lrem",
95
+ "set_add" => "sadd",
96
+ "set_delete" => "srem",
97
+ "set_count" => "scard",
98
+ "set_member?" => "sismember",
99
+ "set_members" => "smembers",
100
+ "set_intersect" => "sinter",
101
+ "set_intersect_store" => "sinterstore",
102
+ "set_inter_store" => "sinterstore",
103
+ "set_union" => "sunion",
104
+ "set_union_store" => "sunionstore",
105
+ "set_diff" => "sdiff",
106
+ "set_diff_store" => "sdiffstore",
107
+ "set_move" => "smove",
108
+ "set_unless_exists" => "setnx",
109
+ "rename_unless_exists" => "renamenx",
110
+ "type?" => "type",
111
+ "zset_add" => "zadd",
112
+ "zset_count" => "zcard",
113
+ "zset_range_by_score" => "zrangebyscore",
114
+ "zset_reverse_range" => "zrevrange",
115
+ "zset_range" => "zrange",
116
+ "zset_delete" => "zrem",
117
+ "zset_score" => "zscore",
118
+ "zset_incr_by" => "zincrby",
119
+ "zset_increment_by" => "zincrby"
120
+ }
121
+
122
+ DISABLED_COMMANDS = {
123
+ "monitor" => true,
124
+ "sync" => true
125
+ }
126
+
127
+ def initialize(options = {})
128
+ @host = options[:host] || '127.0.0.1'
129
+ @port = (options[:port] || 6379).to_i
130
+ @db = (options[:db] || 0).to_i
131
+ @timeout = (options[:timeout] || 5).to_i
132
+ @password = options[:password]
133
+ @logger = options[:logger]
134
+ @thread_safe = options[:thread_safe]
135
+ @binary_keys = options[:binary_keys]
136
+ @mutex = Mutex.new if @thread_safe
137
+ @sock = nil
138
+ @pubsub = false
139
+
140
+ log(self)
141
+ end
142
+
143
+ def to_s
144
+ "Redis Client connected to #{server} against DB #{@db}"
145
+ end
146
+
147
+ def select(*args)
148
+ raise "SELECT not allowed, use the :db option when creating the object"
149
+ end
150
+
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}"
320
+ end
321
+
322
+ def connect_to(host, port, timeout=nil)
323
+ # We support connect() timeout only if system_timer is availabe
324
+ # or if we are running against Ruby >= 1.9
325
+ # Timeout reading from the socket instead will be supported anyway.
326
+ if @timeout != 0 and Timer
327
+ begin
328
+ sock = TCPSocket.new(host, port)
329
+ rescue Timeout::Error
330
+ @sock = nil
331
+ raise Timeout::Error, "Timeout connecting to the server"
332
+ end
333
+ else
334
+ sock = TCPSocket.new(host, port)
335
+ end
336
+ sock.setsockopt Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1
337
+
338
+ # If the timeout is set we set the low level socket options in order
339
+ # to make sure a blocking read will return after the specified number
340
+ # of seconds. This hack is from memcached ruby client.
341
+ if timeout
342
+ secs = Integer(timeout)
343
+ usecs = Integer((timeout - secs) * 1_000_000)
344
+ optval = [secs, usecs].pack("l_2")
345
+ begin
346
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
347
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
348
+ rescue Exception => ex
349
+ # Solaris, for one, does not like/support socket timeouts.
350
+ log("Unable to use raw socket timeouts: #{ex.class.name}: #{ex.message}")
351
+ end
352
+ end
353
+ sock
354
+ end
355
+
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
360
+ end
361
+
362
+ def method_missing(*argv)
363
+ call_command(argv)
364
+ end
365
+
366
+ def raw_call_command(argvp)
367
+ if argvp[0].is_a?(Array)
368
+ argvv = argvp
369
+ pipeline = true
370
+ else
371
+ argvv = [argvp]
372
+ end
373
+
374
+ if @binary_keys or MULTI_BULK_COMMANDS[argvv[0][0].to_s]
375
+ command = ""
376
+ argvv.each do |argv|
377
+ command << "*#{argv.size}\r\n"
378
+ argv.each{|a|
379
+ a = a.to_s
380
+ command << "$#{get_size(a)}\r\n"
381
+ command << a
382
+ command << "\r\n"
383
+ }
384
+ end
385
+ else
386
+ command = ""
387
+ argvv.each do |argv|
388
+ bulk = nil
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
394
+ raise "#{argv[0]} command is disabled" if DISABLED_COMMANDS[argv[0]]
395
+ if BULK_COMMANDS[argv[0]] and argv.length > 1
396
+ bulk = argv[-1].to_s
397
+ argv[-1] = get_size(bulk)
398
+ end
399
+ command << "#{argv.join(' ')}\r\n"
400
+ command << "#{bulk}\r\n" if bulk
401
+ end
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.
409
+ results = maybe_lock { process_command(command, argvv) }
410
+ return pipeline ? results : results[0]
411
+ end
412
+
413
+ def process_command(command, argvv)
414
+ @sock.write(command)
415
+ argvv.map do |argv|
416
+ processor = REPLY_PROCESSOR[argv[0].to_s]
417
+ processor ? processor.call(read_reply) : read_reply
418
+ end
419
+ end
420
+
421
+ def maybe_lock(&block)
422
+ if @thread_safe
423
+ @mutex.synchronize(&block)
424
+ else
425
+ block.call
426
+ end
427
+ end
428
+
429
+ def read_reply
430
+ # We read the first byte using read() mainly because gets() is
431
+ # immune to raw socket timeouts.
432
+ begin
433
+ rtype = @sock.read(1)
434
+ rescue Errno::EAGAIN
435
+ # We want to make sure it reconnects on the next command after the
436
+ # timeout. Otherwise the server may reply in the meantime leaving
437
+ # the protocol in a desync status.
438
+ @sock = nil
439
+ raise Errno::EAGAIN, "Timeout reading from the socket"
440
+ end
441
+
442
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
443
+ line = @sock.gets
444
+ case rtype
445
+ when MINUS
446
+ raise MINUS + line.strip
447
+ when PLUS
448
+ line.strip
449
+ when COLON
450
+ line.to_i
451
+ when DOLLAR
452
+ bulklen = line.to_i
453
+ return nil if bulklen == -1
454
+ data = @sock.read(bulklen)
455
+ @sock.read(2) # CRLF
456
+ data
457
+ when ASTERISK
458
+ objects = line.to_i
459
+ return nil if bulklen == -1
460
+ res = []
461
+ objects.times {
462
+ res << read_reply
463
+ }
464
+ res
465
+ else
466
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
467
+ end
468
+ end
469
+
470
+ def get_size(string)
471
+ string.respond_to?(:bytesize) ? string.bytesize : string.size
472
+ end
473
+
474
+ def log(str, level = :info)
475
+ @logger.send(level, str.to_s) if @logger
476
+ end
477
+
478
+ def deprecated(old, new, trace = caller[0])
479
+ $stderr.puts "\nRedis: The method #{old} is deprecated. Use #{new} instead (in #{trace})"
480
+ end
481
+ end
482
+ end