redis_support 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ Redis Support
2
+ =============
3
+
4
+ redis_support is a small library which provides simple support for
5
+ common actions for the Redis key-value store. It is not an
6
+ object-relational mapper, does not attempt to comprehensively solve
7
+ all of your storage problems, and does not make julienne fries.
8
+
9
+ Key Support
10
+ -----------
11
+
12
+ Redis provides a global keyspace. Most projects specify their keys
13
+ using namespaces delimited by colons (:), with variables mixed in at
14
+ particular points. For example:
15
+
16
+ users:1:email
17
+
18
+ `redis_support`, when used as a mixin, will provide a simple method to
19
+ declare that namespace and access it later.
20
+
21
+ class User
22
+ include RedisSupport
23
+ redis_key :email, "users:USER_ID:email"
24
+
25
+ attr_accessor :id
26
+
27
+ def email
28
+ redis.get Keys.email( self.id )
29
+ end
30
+ end
31
+
32
+ Helpful exceptions are raised if you try to declare the same namespace
33
+ twice - RedisSupport keeps track of all the key definitions your app
34
+ wants to use.
35
+
36
+ Locking Support
37
+ ---------------
38
+
39
+ There is also a simple locking mechanism based on SETNX. Locking is
40
+ usually unnecessary since Redis operations are atomic and Redis'
41
+ MULTI/EXEC/DISCARD gives you the ability to make multiple operations
42
+ atomic. But sometimes it's useful - a contrived example -
43
+ major_operation will block for up to 30 seconds if another process
44
+ attempts to run it at the same time:
45
+
46
+ class User
47
+ include RedisSupport
48
+ redis_key :email, "users:USER_ID:email"
49
+
50
+ attr_accessor :id
51
+
52
+ def major_operation
53
+ redis_lock Keys.email( self.id ) do
54
+ # do my expensive stuff
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,56 @@
1
+ module RedisSupport
2
+ class RedisKeyError < StandardError ; end
3
+ class DuplicateRedisKeyDefinitionError < RedisKeyError ; end
4
+ class InvalidRedisKeyDefinitionError < RedisKeyError ; end
5
+
6
+ VAR_PATTERN = /^[A-Z]+(_[A-Z]+)*$/
7
+ STR_PATTERN = /^[a-z_]+$/
8
+ module ClassMethods
9
+ # Goal is to allow a class to declare a redis key/property
10
+ # The key is a colon delimited string where variables
11
+ # are listed are upper case (underscores inbetween) and
12
+ # non variables are completely lower case (underscores inbetween or appending/prepending)
13
+ #
14
+ # variables cannot be repeated and must start with a letter
15
+ # the key must also start with a nonvariable
16
+ #
17
+ # Examples
18
+ #
19
+ # redis_key :workpools, "job:JOB_ID:workpools"
20
+ #
21
+ # Returns the redis key.
22
+ def redis_key( name, keystruct )
23
+ if Keys.methods.include? name.to_s
24
+ raise DuplicateRedisKeyDefinitionError
25
+ end
26
+
27
+ key = keystruct.split(":")
28
+
29
+ unless (first = key.shift) =~ STR_PATTERN
30
+ raise InvalidRedisKeyDefinitionError.new "keys must begin with lowercase letters"
31
+ end
32
+
33
+ vars, strs = key.inject([[],[]]) do |(vs, ss), token|
34
+ case token
35
+ when VAR_PATTERN
36
+ var = token.downcase
37
+ ss << "\#{#{var}}"
38
+ vs << var
39
+ when STR_PATTERN
40
+ ss << token
41
+ else
42
+ raise InvalidRedisKeyDefinitionError.new "Internal error parsing #{keystruct} : last token : #{token}"
43
+ end
44
+ [vs, ss]
45
+ end
46
+
47
+ strs.unshift(first)
48
+
49
+ RedisSupport::Keys.class_eval <<-RUBY, __FILE__, __LINE__ + 1
50
+ def self.#{name.to_s}( #{vars.map {|x| x.to_s }.join(', ')} )
51
+ "#{strs.join(":")}"
52
+ end
53
+ RUBY
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,98 @@
1
+ # Locking support
2
+ #
3
+ module RedisSupport
4
+
5
+ # Lock a block of code so it can only be accessed by one thread in
6
+ # our system at a time.
7
+ #
8
+ # See 'acquire_redis_lock' for details on parameters.
9
+ #
10
+ # Returns nothing.
11
+ def redis_lock( key_to_lock, expiration = 30, interval = 1 )
12
+ acquire_redis_lock( key_to_lock, expiration, interval )
13
+ yield
14
+ ensure
15
+ release_redis_lock( key_to_lock )
16
+ end
17
+
18
+ # Acquire a lock on a key in our Redis database. This is a blocking
19
+ # call. It sleeps until the lock has been successfully acquired.
20
+ #
21
+ # Basic usage:
22
+ #
23
+ # acquire_redis_lock( key.my_key )
24
+ # # do some stuff on my_key
25
+ # release_redis_lock( key.my_key )
26
+ #
27
+ # Described in detail here:
28
+ #
29
+ # http://code.google.com/p/redis/wiki/SetnxCommand
30
+ #
31
+ # key_to_lock - the key to lock. the actual key for the lock in redis will
32
+ # be this value with 'lock.' prepended, which lets this whole
33
+ # acquire_lock business act like a standard ruby object or
34
+ # synchronize lock. Also it ensures that all locks in the database
35
+ # can be easily viewed using redis.keys("lock.*")
36
+ #
37
+ # expiration - the expiration for the lock, expressed as an Integer. default is
38
+ # 30 seconds from when the lock is acquired. Note that this is the
39
+ # amount of time others will wait for you, not the amount of time
40
+ # you will wait to acquire the lock.
41
+ #
42
+ # interval - sleep interval for checking the lock's status.
43
+ #
44
+ # Returns nothing.
45
+ def acquire_redis_lock( key_to_lock, expiration = 30, interval = 1 )
46
+ key = lock_key( key_to_lock )
47
+ until redis.setnx key, timeout_i( expiration )
48
+ if redis.get( key ).to_i < Time.now.to_i
49
+ old_timeout = redis.getset( key, timeout_i( expiration ) ).to_i
50
+ if old_timeout < Time.now.to_i
51
+ return # got it!
52
+ end
53
+ else
54
+ sleep interval
55
+ end
56
+ end
57
+ end
58
+
59
+ # Acquire a redis lock only if it can be acquired
60
+ # is a nonblocking action
61
+ #
62
+ # Returns true on success and false on failure
63
+ def acquire_redis_lock_nonblock( key_to_lock, expiration = 30 )
64
+ key = lock_key( key_to_lock )
65
+ redis.setnx key, timeout_i( expiration )
66
+ end
67
+
68
+ # See docs for acquire_redis_lock above
69
+ #
70
+ # Returns nothing.
71
+ def release_redis_lock( locked_key )
72
+ redis.del lock_key( locked_key )
73
+ end
74
+
75
+ def has_redis_lock?( locked_key )
76
+ redis.exists lock_key(locked_key)
77
+ end
78
+
79
+ private
80
+
81
+ def lock_key( key_to_lock )
82
+ "lock.#{key_to_lock}"
83
+ end
84
+
85
+ # Converts an Integer number of seconds into a future timestamp that
86
+ # can be used with Redis.
87
+ #
88
+ # Examples
89
+ #
90
+ # timeout_i(expiration)
91
+ # # => 1274955869
92
+ #
93
+ # Returns the timestamp.
94
+ def timeout_i( timeout )
95
+ timeout.seconds.from_now.to_i
96
+ end
97
+
98
+ end
@@ -0,0 +1,41 @@
1
+ require 'redis'
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "/"))
4
+ require 'redis_support/class_extensions'
5
+ require 'redis_support/locks'
6
+
7
+ module RedisSupport
8
+
9
+ # Inspired/take from the redis= in Resque
10
+ #
11
+ # Accepts:
12
+ # 1. A 'hostname:port' string
13
+ # 2. A 'hostname:port:db' string (to select the Redis db)
14
+ # 3. An instance of `Redis`, `Redis::Client`
15
+ def redis=(connection)
16
+ if connection.respond_to? :split
17
+ host, port, db = connection.split(':')
18
+ @@redis = Redis.new(:host => host,:port => port,:thread_safe => true,:db => db)
19
+ else
20
+ @@redis = connection
21
+ end
22
+ end
23
+
24
+ def redis
25
+ return @@redis if @@redis
26
+ self.redis = @@redis || 'localhost:6379'
27
+ self.redis
28
+ end
29
+
30
+ def keys
31
+ Keys
32
+ end
33
+
34
+ module Keys ; end
35
+
36
+ def self.included(model)
37
+ model.extend ClassMethods
38
+ model.extend RedisSupport
39
+ end
40
+
41
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,70 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+
4
+ dir = File.dirname(__FILE__)
5
+ $LOAD_PATH.unshift(File.join(dir, '..', 'lib'))
6
+ $LOAD_PATH.unshift(dir)
7
+ require 'redis_support'
8
+
9
+ ##
10
+ # much of this was taken directly from the resque test suite
11
+ #
12
+ #
13
+ if !system("which redis-server")
14
+ puts '', "** can't find `redis-server` in your path"
15
+ puts "** try running `sudo rake install`"
16
+ abort ''
17
+ end
18
+
19
+ #
20
+ # start our own redis when the tests start,
21
+ # kill it when they end
22
+ #
23
+ at_exit do
24
+ next if $!
25
+
26
+ if defined?(MiniTest)
27
+ exit_code = MiniTest::Unit.new.run(ARGV)
28
+ else
29
+ exit_code = Test::Unit::AutoRunner.run
30
+ end
31
+
32
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
33
+ puts "Killing test redis server..."
34
+ `rm -f #{dir}/dump.rdb`
35
+ Process.kill("KILL", pid.to_i)
36
+ exit exit_code
37
+ end
38
+
39
+ puts "Starting redis for testing at localhost:9736..."
40
+ `redis-server #{dir}/redis-test.conf`
41
+
42
+ class TestClass
43
+ include RedisSupport
44
+
45
+ redis_key :test_novar, "test:redis"
46
+ redis_key :test_var, "test:redis:VAR"
47
+ redis_key :test_vars, "test:redis:VAR_ONE:VAR_TWO:append"
48
+ end
49
+
50
+ TestClass.redis = "localhost:9736"
51
+
52
+ ##
53
+ # test/spec/mini 3
54
+ # http://gist.github.com/25455
55
+ # chris@ozmm.org
56
+ #
57
+ def context(*args, &block)
58
+ return super unless (name = args.first) && block
59
+ require 'test/unit'
60
+ klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
61
+ def self.test(name, &block)
62
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
63
+ end
64
+ def self.xtest(*args) end
65
+ def self.setup(&block) define_method(:setup, &block) end
66
+ def self.teardown(&block) define_method(:teardown, &block) end
67
+ end
68
+ (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
69
+ klass.class_eval &block
70
+ end
@@ -0,0 +1,54 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ context "Redis Support" do
4
+ setup do
5
+ TestClass.redis.flushall
6
+ @test_class = TestClass.new
7
+ end
8
+
9
+ test "redis connection works as expected" do
10
+ assert_equal @test_class.redis, TestClass.redis
11
+ assert_equal "OK", @test_class.redis.set("superman", 1)
12
+ assert_equal "1", @test_class.redis.get("superman")
13
+ end
14
+
15
+ test "redis connections changes as expected" do
16
+ TestClass.redis = "localhost:6379"
17
+ assert_equal @test_class.redis, TestClass.redis
18
+ @test_class.redis = "localhost:9736"
19
+ assert_equal @test_class.redis, TestClass.redis
20
+ end
21
+
22
+ test "redis keys are created correctly in normal conditions" do
23
+ assert_equal "test:redis", TestClass::Keys.test_novar
24
+ assert_equal "test:redis:variable", TestClass::Keys.test_var("variable")
25
+ assert_equal "test:redis:variable:id:append", TestClass::Keys.test_vars("variable", "id")
26
+ end
27
+
28
+ test "redis key should be able to create key" do
29
+ assert_nothing_raised do
30
+ TestClass.redis_key :whatever, "this_should_work"
31
+ end
32
+ end
33
+
34
+ test "redis keys are not created if the keyname was previously used" do
35
+ assert_raise(RedisSupport::DuplicateRedisKeyDefinitionError) do
36
+ TestClass.redis_key :test_var, "this:should:fail"
37
+ end
38
+ end
39
+
40
+ test "redis key should fail when given incorrect syntax" do
41
+ failure_keys = %w{oh:WE_should:fail oh:WE_:fail oh:_WE:fail oh:WE_903:fail FAILPART:oh:no}
42
+ failure_keys.each do |failure_key|
43
+ assert_raise(RedisSupport::InvalidRedisKeyDefinitionError) do
44
+ TestClass.redis_key :failure, failure_key
45
+ end
46
+ end
47
+ end
48
+
49
+ test "redis keys fails gracefully, syntax error, when key space is fucked" do
50
+ assert_raise(SyntaxError) do
51
+ TestClass.redis_key :failure, "test:redis:VAR:VAR:oops"
52
+ end
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: redis_support
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - dolores
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-06-18 00:00:00 -07:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: redis
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 31
30
+ segments:
31
+ - 1
32
+ - 0
33
+ - 4
34
+ version: 1.0.4
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: "Module for adding redis functionality to classes: simple key namespacing and locking and connections"
38
+ email: dolores@doloreslabs.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - README.md
45
+ files:
46
+ - lib/redis_support.rb
47
+ - lib/redis_support/class_extensions.rb
48
+ - lib/redis_support/locks.rb
49
+ - README.md
50
+ - test/helper.rb
51
+ - test/test_redis_support.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/dolores/redis_support
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ hash: 3
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ hash: 3
76
+ segments:
77
+ - 0
78
+ version: "0"
79
+ requirements: []
80
+
81
+ rubyforge_project:
82
+ rubygems_version: 1.3.7
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: A Redis Support module
86
+ test_files:
87
+ - test/helper.rb
88
+ - test/test_redis_support.rb