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