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 +58 -0
- data/lib/redis_support/class_extensions.rb +56 -0
- data/lib/redis_support/locks.rb +98 -0
- data/lib/redis_support.rb +41 -0
- data/test/helper.rb +70 -0
- data/test/test_redis_support.rb +54 -0
- metadata +88 -0
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
|