redis_support 0.0.1

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