redis-objects 0.1.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/ATOMICITY.rdoc +154 -0
- data/README.rdoc +144 -0
- data/lib/redis/counter.rb +105 -0
- data/lib/redis/data_types.rb +84 -0
- data/lib/redis/list.rb +95 -0
- data/lib/redis/lock.rb +47 -0
- data/lib/redis/objects.rb +103 -0
- data/lib/redis/objects/core.rb +0 -0
- data/lib/redis/objects/counters.rb +113 -0
- data/lib/redis/objects/lists.rb +34 -0
- data/lib/redis/objects/locks.rb +56 -0
- data/lib/redis/objects/sets.rb +9 -0
- data/lib/redis/objects/values.rb +37 -0
- data/lib/redis/set.rb +7 -0
- data/lib/redis/value.rb +39 -0
- data/spec/redis_objects_model_spec.rb +278 -0
- data/spec/spec_helper.rb +5 -0
- metadata +73 -0
data/lib/redis/list.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
class Redis
|
2
|
+
#
|
3
|
+
# Class representing a Redis list. Instances of Redis::List are designed to
|
4
|
+
# behave as much like Ruby arrays as possible.
|
5
|
+
#
|
6
|
+
class List
|
7
|
+
attr_reader :key, :options, :redis
|
8
|
+
def initialize(key, options={})
|
9
|
+
@key = key
|
10
|
+
@options = options
|
11
|
+
@redis = options[:redis] || $redis || Redis::Objects.redis
|
12
|
+
@options[:start] ||= 0
|
13
|
+
@options[:type] ||= @options[:start] == 0 ? :increment : :decrement
|
14
|
+
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
15
|
+
end
|
16
|
+
|
17
|
+
def <<(value)
|
18
|
+
push(value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def push(value)
|
22
|
+
redis.rpush(key, value)
|
23
|
+
@values << value
|
24
|
+
end
|
25
|
+
|
26
|
+
def pop
|
27
|
+
redis.rpop(key)
|
28
|
+
@values.pop
|
29
|
+
end
|
30
|
+
|
31
|
+
def unshift(value)
|
32
|
+
redis.lpush(key, value)
|
33
|
+
@values.unshift value
|
34
|
+
end
|
35
|
+
|
36
|
+
def shift
|
37
|
+
redis.lpop(key)
|
38
|
+
end
|
39
|
+
|
40
|
+
def values
|
41
|
+
@values ||= get
|
42
|
+
end
|
43
|
+
|
44
|
+
def value=(val)
|
45
|
+
redis.set(key, val)
|
46
|
+
@values = val
|
47
|
+
end
|
48
|
+
|
49
|
+
def get
|
50
|
+
@values = range(0, -1)
|
51
|
+
end
|
52
|
+
|
53
|
+
def [](index)
|
54
|
+
case index
|
55
|
+
when Range
|
56
|
+
range(index.first, index.last)
|
57
|
+
else
|
58
|
+
range(index, index)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def delete(name, count=0)
|
63
|
+
redis.lrem(name, count)
|
64
|
+
end
|
65
|
+
|
66
|
+
def range(start_index, end_index)
|
67
|
+
redis.lrange(key, start_index, end_index)
|
68
|
+
end
|
69
|
+
|
70
|
+
def at(index)
|
71
|
+
redis.lrange(key, index, index)
|
72
|
+
end
|
73
|
+
|
74
|
+
def last
|
75
|
+
redis.lrange(key, -1, -1)
|
76
|
+
end
|
77
|
+
|
78
|
+
def clear
|
79
|
+
redis.del(key)
|
80
|
+
end
|
81
|
+
|
82
|
+
def length
|
83
|
+
redis.length
|
84
|
+
end
|
85
|
+
alias_method :size, :length
|
86
|
+
|
87
|
+
def empty?
|
88
|
+
values.empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
def ==(x)
|
92
|
+
values == x
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/redis/lock.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
class Redis
|
2
|
+
#
|
3
|
+
# Class representing a lock. This functions like a proxy class, in
|
4
|
+
# that you can say @object.lock_name { block } to use the lock and also
|
5
|
+
# @object.counter_name.clear to reset on it. You can use this
|
6
|
+
# directly, but it is better to use the lock :foo class method in your
|
7
|
+
# class to define a lock.
|
8
|
+
#
|
9
|
+
class Lock
|
10
|
+
class LockTimeout < StandardError; end #:nodoc:
|
11
|
+
|
12
|
+
attr_reader :key, :options, :redis
|
13
|
+
def initialize(key, options={})
|
14
|
+
@key = key
|
15
|
+
@options = options
|
16
|
+
@options[:timeout] ||= 5
|
17
|
+
@redis = options[:redis] || $redis || Redis::Objects.redis
|
18
|
+
@redis.setnx(key, @options[:start]) unless @options[:start] == 0 || @options[:init] === false
|
19
|
+
end
|
20
|
+
|
21
|
+
# Clear the lock. Should only be needed if there's a server crash
|
22
|
+
# or some other event that gets locks in a stuck state.
|
23
|
+
def clear
|
24
|
+
redis.del(key)
|
25
|
+
end
|
26
|
+
alias_method :delete, :clear
|
27
|
+
|
28
|
+
# Get the lock and execute the code block. Any other code that needs the lock
|
29
|
+
# (on any server) will spin waiting for the lock up to the :timeout
|
30
|
+
# that was specified when the lock was defined.
|
31
|
+
def lock(&block)
|
32
|
+
start = Time.now
|
33
|
+
gotit = false
|
34
|
+
while Time.now - start < @options[:timeout]
|
35
|
+
gotit = redis.setnx(key, 1)
|
36
|
+
break if gotit
|
37
|
+
sleep 0.1
|
38
|
+
end
|
39
|
+
raise LockTimeout, "Timeout on lock #{key} exceeded #{@options[:timeout]} sec" unless gotit
|
40
|
+
begin
|
41
|
+
yield
|
42
|
+
ensure
|
43
|
+
redis.del(key)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# Redis::Objects - Lightweight object layer around redis-rb
|
2
|
+
# See README.rdoc for usage and approach.
|
3
|
+
require 'redis'
|
4
|
+
class Redis
|
5
|
+
#
|
6
|
+
# Redis::Objects enables high-performance atomic operations in your app
|
7
|
+
# by leveraging the atomic features of the Redis server. To use Redis::Objects,
|
8
|
+
# first include it in any class you want. (This example uses an ActiveRecord
|
9
|
+
# subclass, but that is *not* required.) Then, use +counter+ and +lock+
|
10
|
+
# to define your primitives:
|
11
|
+
#
|
12
|
+
# class Game < ActiveRecord::Base
|
13
|
+
# include Redis::Objects
|
14
|
+
#
|
15
|
+
# counter :joined_players
|
16
|
+
# counter :active_players
|
17
|
+
# set :player_ids
|
18
|
+
# lock :archive_game
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# The, you can use these counters both for bookeeping and as atomic actions:
|
22
|
+
#
|
23
|
+
# @game = Game.find(id)
|
24
|
+
# @game_user = @game.joined_players.increment do |val|
|
25
|
+
# break if val > @game.max_players
|
26
|
+
# gu = @game.game_users.create!(:user_id => @user.id)
|
27
|
+
# @game.active_players.increment
|
28
|
+
# gu
|
29
|
+
# end
|
30
|
+
# if @game_user.nil?
|
31
|
+
# # game is full - error screen
|
32
|
+
# else
|
33
|
+
# # success
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
#
|
37
|
+
#
|
38
|
+
module Objects
|
39
|
+
dir = File.expand_path(__FILE__.sub(/\.rb$/,''))
|
40
|
+
|
41
|
+
autoload :Counters, File.join(dir, 'counters')
|
42
|
+
autoload :Values, File.join(dir, 'values')
|
43
|
+
autoload :Lists, File.join(dir, 'lists')
|
44
|
+
autoload :Sets, File.join(dir, 'sets')
|
45
|
+
autoload :Locks, File.join(dir, 'locks')
|
46
|
+
|
47
|
+
class NotConnected < StandardError; end
|
48
|
+
|
49
|
+
class << self
|
50
|
+
def redis=(conn) @redis = conn end
|
51
|
+
def redis
|
52
|
+
@redis ||= $redis || raise(NotConnected, "Redis::Objects.redis not set to a Redis.new connection")
|
53
|
+
end
|
54
|
+
|
55
|
+
def included(klass)
|
56
|
+
# Core (this file)
|
57
|
+
klass.instance_variable_set('@redis', @redis)
|
58
|
+
klass.send :include, InstanceMethods
|
59
|
+
klass.extend ClassMethods
|
60
|
+
|
61
|
+
# Adapted from Redis::Model for marshaling complex data
|
62
|
+
require 'redis/data_types'
|
63
|
+
klass.send :include, Redis::DataTypes
|
64
|
+
|
65
|
+
# Pull in each object type
|
66
|
+
klass.send :include, Redis::Objects::Counters
|
67
|
+
klass.send :include, Redis::Objects::Values
|
68
|
+
klass.send :include, Redis::Objects::Lists
|
69
|
+
klass.send :include, Redis::Objects::Sets
|
70
|
+
klass.send :include, Redis::Objects::Locks
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
75
|
+
module ClassMethods
|
76
|
+
attr_accessor :redis
|
77
|
+
|
78
|
+
|
79
|
+
# Set the Redis prefix to use. Defaults to model_name
|
80
|
+
def prefix=(prefix) @prefix = prefix end
|
81
|
+
def prefix #:nodoc:
|
82
|
+
@prefix ||= self.name.to_s.
|
83
|
+
sub(%r{(.*::)}, '').
|
84
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
85
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
86
|
+
downcase
|
87
|
+
end
|
88
|
+
|
89
|
+
def field_key(name, id) #:nodoc:
|
90
|
+
"#{prefix}:#{id}:#{name}"
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
96
|
+
module InstanceMethods
|
97
|
+
def redis() self.class.redis end
|
98
|
+
def field_key(name) #:nodoc:
|
99
|
+
self.class.field_key(name, id)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
File without changes
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# This is the class loader, for use as "include Redis::Objects::Counters"
|
2
|
+
# For the object itself, see "Redis::Counter"
|
3
|
+
require 'redis/counter'
|
4
|
+
class Redis
|
5
|
+
module Objects
|
6
|
+
class UndefinedCounter < StandardError; end #:nodoc:
|
7
|
+
module Counters
|
8
|
+
def self.included(klass)
|
9
|
+
klass.instance_variable_set('@counters', {})
|
10
|
+
klass.instance_variable_set('@initialized_counters', {})
|
11
|
+
klass.send :include, InstanceMethods
|
12
|
+
klass.extend ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
16
|
+
module ClassMethods
|
17
|
+
attr_reader :counters, :initialized_counters
|
18
|
+
|
19
|
+
# Define a new counter. It will function like a regular instance
|
20
|
+
# method, so it can be used alongside ActiveRecord, DataMapper, etc.
|
21
|
+
def counter(name, options={})
|
22
|
+
options[:start] ||= 0
|
23
|
+
options[:type] ||= options[:start] == 0 ? :increment : :decrement
|
24
|
+
@counters[name] = options
|
25
|
+
class_eval <<-EndMethods
|
26
|
+
def #{name}
|
27
|
+
@#{name} ||= Redis::Counter.new(field_key(:#{name}), self.class.counters[:#{name}].merge(:redis => redis))
|
28
|
+
end
|
29
|
+
EndMethods
|
30
|
+
end
|
31
|
+
|
32
|
+
# Get the current value of the counter. It is more efficient
|
33
|
+
# to use the instance method if possible.
|
34
|
+
def get_counter(name, id)
|
35
|
+
verify_counter_defined!(name)
|
36
|
+
initialize_counter!(name, id)
|
37
|
+
redis.get(field_key(name, id)).to_i
|
38
|
+
end
|
39
|
+
|
40
|
+
# Increment a counter with the specified name and id. Accepts a block
|
41
|
+
# like the instance method. See Redis::Objects::Counter for details.
|
42
|
+
def increment_counter(name, id, by=1, &block)
|
43
|
+
verify_counter_defined!(name)
|
44
|
+
initialize_counter!(name, id)
|
45
|
+
value = redis.incr(field_key(name, id), by).to_i
|
46
|
+
block_given? ? rewindable_block(:decrement_counter, name, id, value, &block) : value
|
47
|
+
end
|
48
|
+
|
49
|
+
# Decrement a counter with the specified name and id. Accepts a block
|
50
|
+
# like the instance method. See Redis::Objects::Counter for details.
|
51
|
+
def decrement_counter(name, id, by=1, &block)
|
52
|
+
verify_counter_defined!(name)
|
53
|
+
initialize_counter!(name, id)
|
54
|
+
value = redis.decr(field_key(name, id), by).to_i
|
55
|
+
block_given? ? rewindable_block(:increment_counter, name, id, value, &block) : value
|
56
|
+
end
|
57
|
+
|
58
|
+
# Reset a counter to its starting value.
|
59
|
+
def reset_counter(name, id, to=nil)
|
60
|
+
verify_counter_defined!(name)
|
61
|
+
to = @counters[name][:start] if to.nil?
|
62
|
+
redis.set(field_key(name, id), to)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def verify_counter_defined!(name) #:nodoc:
|
68
|
+
raise Redis::Objects::UndefinedCounter, "Undefined counter :#{name} for class #{self.name}" unless @counters.has_key?(name)
|
69
|
+
end
|
70
|
+
|
71
|
+
def initialize_counter!(name, id) #:nodoc:
|
72
|
+
key = field_key(name, id)
|
73
|
+
unless @initialized_counters[key]
|
74
|
+
redis.setnx(key, @counters[name][:start])
|
75
|
+
end
|
76
|
+
@initialized_counters[key] = true
|
77
|
+
end
|
78
|
+
|
79
|
+
# Implements increment/decrement blocks on a class level
|
80
|
+
def rewindable_block(rewind, name, id, value, &block) #:nodoc:
|
81
|
+
# Unfortunately this is almost exactly duplicated from Redis::Counter
|
82
|
+
raise ArgumentError, "Missing block to rewindable_block somehow" unless block_given?
|
83
|
+
ret = nil
|
84
|
+
begin
|
85
|
+
ret = yield value
|
86
|
+
rescue
|
87
|
+
send(rewind, name, id)
|
88
|
+
raise
|
89
|
+
end
|
90
|
+
send(rewind, name, id) if ret.nil?
|
91
|
+
ret
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
96
|
+
module InstanceMethods
|
97
|
+
# Increment a counter.
|
98
|
+
# It is more efficient to use increment_[counter_name] directly.
|
99
|
+
# This is mainly just for completeness to override ActiveRecord.
|
100
|
+
def increment(name, by=1)
|
101
|
+
send(name).increment(by)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Decrement a counter.
|
105
|
+
# It is more efficient to use increment_[counter_name] directly.
|
106
|
+
# This is mainly just for completeness to override ActiveRecord.
|
107
|
+
def decrement(name, by=1)
|
108
|
+
send(name).decrement(by)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# This is the class loader, for use as "include Redis::Objects::Lists"
|
2
|
+
# For the object itself, see "Redis::List"
|
3
|
+
require 'redis/list'
|
4
|
+
class Redis
|
5
|
+
module Objects
|
6
|
+
module Lists
|
7
|
+
def self.included(klass)
|
8
|
+
klass.instance_variable_set('@lists', {})
|
9
|
+
klass.send :include, InstanceMethods
|
10
|
+
klass.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
14
|
+
module ClassMethods
|
15
|
+
attr_reader :lists
|
16
|
+
|
17
|
+
# Define a new list. It will function like a regular instance
|
18
|
+
# method, so it can be used alongside ActiveRecord, DataMapper, etc.
|
19
|
+
def list(name, options={})
|
20
|
+
@lists[name] = options
|
21
|
+
class_eval <<-EndMethods
|
22
|
+
def #{name}
|
23
|
+
@#{name} ||= Redis::List.new(field_key(:#{name}), self.class.lists[:#{name}].merge(:redis => redis))
|
24
|
+
end
|
25
|
+
EndMethods
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Instance methods that appear in your class when you include Redis::Objects.
|
30
|
+
module InstanceMethods
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# This is the class loader, for use as "include Redis::Objects::Locks"
|
2
|
+
# For the object itself, see "Redis::Lock"
|
3
|
+
require 'redis/lock'
|
4
|
+
class Redis
|
5
|
+
module Objects
|
6
|
+
class UndefinedLock < StandardError; end #:nodoc:
|
7
|
+
module Locks
|
8
|
+
def self.included(klass)
|
9
|
+
klass.instance_variable_set('@locks', {})
|
10
|
+
klass.send :include, InstanceMethods
|
11
|
+
klass.extend ClassMethods
|
12
|
+
end
|
13
|
+
|
14
|
+
# Class methods that appear in your class when you include Redis::Objects.
|
15
|
+
module ClassMethods
|
16
|
+
attr_reader :locks
|
17
|
+
|
18
|
+
# Define a new lock. It will function like a model attribute,
|
19
|
+
# so it can be used alongside ActiveRecord/DataMapper, etc.
|
20
|
+
def lock(name, options={})
|
21
|
+
options[:timeout] ||= 5 # seconds
|
22
|
+
@locks[name] = options
|
23
|
+
class_eval <<-EndMethods
|
24
|
+
def #{name}_lock(&block)
|
25
|
+
@#{name}_lock ||= Redis::Lock.new(field_key(:#{name}_lock), self.class.locks[:#{name}].merge(:redis => redis))
|
26
|
+
end
|
27
|
+
EndMethods
|
28
|
+
end
|
29
|
+
|
30
|
+
# Obtain a lock, and execute the block synchronously. Any other code
|
31
|
+
# (on any server) will spin waiting for the lock up to the :timeout
|
32
|
+
# that was specified when the lock was defined.
|
33
|
+
def obtain_lock(name, id, &block)
|
34
|
+
verify_lock_defined!(name)
|
35
|
+
raise ArgumentError, "Missing block to #{self.name}.obtain_lock" unless block_given?
|
36
|
+
lock_name = field_key("#{name}_lock", id)
|
37
|
+
Redis::Lock.new(redis, lock_name, self.class.locks[name]).lock(&block)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Clear the lock. Use with care - usually only in an Admin page to clear
|
41
|
+
# stale locks (a stale lock should only happen if a server crashes.)
|
42
|
+
def clear_lock(name, id)
|
43
|
+
verify_lock_defined!(name)
|
44
|
+
lock_name = field_key("#{name}_lock", id)
|
45
|
+
redis.del(lock_name)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def verify_lock_defined!(name)
|
51
|
+
raise Redis::Objects::UndefinedLock, "Undefined lock :#{name} for class #{self.name}" unless @locks.has_key?(name)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|