redpear 0.6.4 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,9 @@
1
1
  module Redpear::Schema
2
- extend Redpear::Concern
3
2
  autoload :Collection, 'redpear/schema/collection'
3
+ autoload :Column, 'redpear/schema/column'
4
+ autoload :Index, 'redpear/schema/index'
5
+ autoload :Score, 'redpear/schema/score'
6
+ extend Redpear::Concern
4
7
 
5
8
  module ClassMethods
6
9
 
@@ -9,23 +12,23 @@ module Redpear::Schema
9
12
  @columns ||= Redpear::Schema::Collection.new
10
13
  end
11
14
 
12
- # @param [multiple] the column definition. Please see Redpear::Column#initialize
15
+ # @param [multiple] the column definition. Please see Redpear::Schema::Column#initialize
13
16
  def column(*args)
14
- columns.store(Redpear::Column, self, *args).tap do |col|
17
+ columns.store(Redpear::Schema::Column, self, *args).tap do |col|
15
18
  __define_attribute_accessors__(col)
16
19
  end
17
20
  end
18
21
 
19
22
  # @param [multiple] the index definition. Please see Redpear::Index#initialize
20
23
  def index(*args)
21
- columns.store(Redpear::Index, self, *args).tap do |col|
24
+ columns.store(Redpear::Schema::Index, self, *args).tap do |col|
22
25
  __define_attribute_accessors__(col)
23
26
  end
24
27
  end
25
28
 
26
29
  # @param [multiple] the sorted index definition. Please see Redpear::ZIndex#initialize
27
- def zindex(*args)
28
- columns.store(Redpear::ZIndex, self, *args).tap do |col|
30
+ def score(*args)
31
+ columns.store(Redpear::Schema::Score, self, *args).tap do |col|
29
32
  __define_attribute_accessors__(col)
30
33
  end
31
34
  end
@@ -0,0 +1,120 @@
1
+ require 'securerandom'
2
+
3
+ class Redpear::Store::Base
4
+
5
+ attr_reader :key, :conn
6
+
7
+ # Creates and yields over a temporary key.
8
+ # Useful in combination with e.g. `interstore`, `unionstore`, etc.
9
+ #
10
+ # @param [Redpear::Connection] conn
11
+ # The connection
12
+ # @param [Hash] options
13
+ # The options hash
14
+ # @option [String] prefix
15
+ # Specify a key prefix. Example:
16
+ # Base.temporary conn, :prefix => "temp:" do |c|
17
+ # store.key # => temp:55ee0c1ec9530cf545bc25040beb4f292fd448af
18
+ # end
19
+ # @yield [Redpear::Store::Base]
20
+ # The temporary key
21
+ def self.temporary(conn, options = {})
22
+ store = nil
23
+ while !store || store.exists?
24
+ key = "#{options[:prefix]}#{SecureRandom.hex(20)}"
25
+ store = new(key, conn)
26
+ end
27
+ yield store
28
+ ensure
29
+ store.clear if store
30
+ end
31
+
32
+ # Constructor
33
+ # @param [String] key
34
+ # The storage key
35
+ # @param [Redpear::Connection] conn
36
+ # The connection
37
+ def initialize(key, conn)
38
+ @key, @conn = key, conn
39
+ end
40
+
41
+ alias to_s key
42
+
43
+ # @return [String] custom inspect
44
+ def inspect
45
+ "#<#{self.class.name} #{key}: #{value.inspect}>"
46
+ end
47
+
48
+ # @return [Boolean] true if the record exists
49
+ def exists?
50
+ !!conn.exists(key)
51
+ end
52
+
53
+ # @return [Integer] remaining time-to-live in seconds (if set)
54
+ def ttl
55
+ value = conn.ttl(key).to_i
56
+ value if value > -1
57
+ end
58
+
59
+ # @return [String] type information for this record
60
+ def type
61
+ conn.type(key).to_sym
62
+ end
63
+
64
+ # Expires the record
65
+ # @overload expire(time)
66
+ # @param [Time] time The time to expire the record at
67
+ # @overload expire(seconds)
68
+ # @param [Integer] seconds Expire in `seconds` from now
69
+ def expire(expiration)
70
+ case expiration
71
+ when Time
72
+ expire_at(expiration)
73
+ when Integer
74
+ expire_in(expiration)
75
+ when String
76
+ expiration = Kernel::Integer(expiration) rescue nil
77
+ expire(expiration)
78
+ else
79
+ false
80
+ end
81
+ end
82
+
83
+ # Deletes the whole record
84
+ def purge!
85
+ conn.del(key) == 1
86
+ end
87
+
88
+ # Deletes the record and returns the value
89
+ def clear
90
+ purge!
91
+ value
92
+ end
93
+
94
+ # Expires the record
95
+ # @param [Time] time The time to expire the record at
96
+ def expire_at(time)
97
+ conn.expireat key, time.to_i
98
+ end
99
+
100
+ # Expires the record
101
+ # @param [Integer] seconds Expire in `seconds` from now
102
+ def expire_in(seconds)
103
+ conn.expire key, seconds.to_i
104
+ end
105
+
106
+ # @abstract, override in subclasses
107
+ def value
108
+ nil
109
+ end
110
+
111
+ private
112
+
113
+ def range_pair(range)
114
+ first = range.first.to_i
115
+ last = range.last.to_i
116
+ last -= 1 if range.exclude_end?
117
+ [first, last]
118
+ end
119
+
120
+ end
@@ -0,0 +1,44 @@
1
+ class Redpear::Store::Counter < Redpear::Store::Value
2
+
3
+ # @return [Integer] the value
4
+ def get
5
+ super.to_i
6
+ end
7
+
8
+ # Sets the value
9
+ # @param [Integer] the value to set
10
+ def set(value)
11
+ super Kernel::Integer(value)
12
+ end
13
+
14
+ undef_method :append
15
+ undef_method :<<
16
+
17
+ # Increments the value
18
+ # @param [Integer] by
19
+ # The increment, defaults to 1
20
+ def increment(by = 1)
21
+ case by
22
+ when 1
23
+ conn.incr(key)
24
+ else
25
+ conn.incrby(key, by)
26
+ end
27
+ end
28
+ alias_method :next, :increment
29
+
30
+ # Decrements the value
31
+ # @param [Integer] by
32
+ # The decrement, defaults to 1
33
+ def decrement(by = 1)
34
+ case by
35
+ when 1
36
+ conn.decr(key)
37
+ else
38
+ conn.decrby(key, by)
39
+ end
40
+ end
41
+ alias_method :previous, :decrement
42
+ alias_method :prev, :decrement
43
+
44
+ end
@@ -0,0 +1,8 @@
1
+ class Redpear::Store::Enumerable < Redpear::Store::Base
2
+ include ::Enumerable
3
+
4
+ # Returns the array as the record's value
5
+ # @see Redpear::Store::Base#value
6
+ alias_method :value, :to_a
7
+
8
+ end
@@ -0,0 +1,120 @@
1
+ class Redpear::Store::Hash < Redpear::Store::Enumerable
2
+
3
+ # @yield over a field-value pair
4
+ # @yieldparam [String] field
5
+ # @yieldparam [String] value
6
+ def each(&block)
7
+ all.each(&block)
8
+ end
9
+
10
+ # @return [Hash] all pairs
11
+ def all
12
+ conn.hgetall(key) || {}
13
+ end
14
+ alias_method :to_hash, :all
15
+ alias_method :value, :all
16
+
17
+ # @param [String] field
18
+ # The field to delete
19
+ def delete(field)
20
+ conn.hdel key, field
21
+ end
22
+
23
+ # @return [Boolean] true, if field exists
24
+ def key?(field)
25
+ !!conn.hexists(key, field)
26
+ end
27
+ alias_method :has_key?, :key?
28
+ alias_method :member?, :key?
29
+ alias_method :include?, :key?
30
+
31
+ # @return [Boolean] true, if empty
32
+ def empty?
33
+ length.zero?
34
+ end
35
+
36
+ # @param [String] field
37
+ # The field to fetch
38
+ # @return [String] value stored in +field+
39
+ def fetch(field)
40
+ conn.hget key, field
41
+ end
42
+ alias_method :[], :fetch
43
+
44
+ # @param [String] field
45
+ # The field to store at
46
+ # @param [String] value
47
+ # The value to store
48
+ # @param [Hash] options
49
+ # The value to store
50
+ def store(field, value, options = {})
51
+ if value.nil?
52
+ delete field
53
+ else
54
+ conn.hset key, field, value
55
+ end
56
+ end
57
+ alias_method :[]=, :store
58
+
59
+ # @return [Array] all keys
60
+ def keys
61
+ conn.hkeys key
62
+ end
63
+
64
+ # @return [Array] all values
65
+ def values
66
+ conn.hvals key
67
+ end
68
+
69
+ # @return [Intege] the number of pairs in the hash
70
+ def length
71
+ conn.hlen key
72
+ end
73
+ alias_method :size, :length
74
+
75
+ # @param [String] field
76
+ # The field to increment
77
+ # @param [Integer] value
78
+ # The increment value, defaults to 1
79
+ def increment(field, value = 1)
80
+ conn.hincrby key, field, value
81
+ end
82
+
83
+ # @param [String] field
84
+ # The field to decrement
85
+ # @param [Integer] value
86
+ # The decrement value, defaults to 1
87
+ def decrement(field, value = 1)
88
+ increment(field, -value)
89
+ end
90
+
91
+ # @param [multiple] fields
92
+ # The field to return
93
+ # @return [Array] values
94
+ def values_at(*fields)
95
+ conn.hmget key, *fields
96
+ end
97
+
98
+ # @param [Hash] hash
99
+ # The pairs to update
100
+ def update(hash)
101
+ merge!(hash)
102
+ to_hash
103
+ end
104
+
105
+ # @param [Hash] hash
106
+ # The pairs to merge
107
+ def merge!(hash)
108
+ hash = hash.reject do |field, value|
109
+ delete(field) if value.nil?
110
+ end
111
+ conn.hmset key, *hash.flatten
112
+ end
113
+
114
+ # Comparator
115
+ # @return [Boolean] true if same as `other`
116
+ def ==(other)
117
+ other.respond_to?(:to_hash) && other.to_hash == to_hash
118
+ end
119
+
120
+ end
@@ -0,0 +1,144 @@
1
+ class Redpear::Store::List < Redpear::Store::Enumerable
2
+
3
+ # @yield over each item in the list
4
+ # @yieldparam [String] item
5
+ def each(&block)
6
+ all.each(&block)
7
+ end
8
+
9
+ # @return [Array] all items
10
+ def all
11
+ slice(0..-1)
12
+ end
13
+ alias_method :to_a, :all
14
+
15
+ # Returns a slice of the list
16
+ # @overload slice(index)
17
+ # Returns the item at `index`
18
+ # @param [Integer] index
19
+ # @return [String] item
20
+ # @overload slice(start, length)
21
+ # Returns from `length` items from `start`
22
+ # @param [Integer] start
23
+ # @param [Integer] length
24
+ # @return [Array] items
25
+ # @overload slice(range)
26
+ # Returns items from range
27
+ # @param [Range] range
28
+ # @return [Array] items
29
+ def slice(start, length = nil)
30
+ case start
31
+ when Integer
32
+ if length
33
+ range(start, start + length - 1)
34
+ else
35
+ conn.lindex(key, start) rescue nil
36
+ end
37
+ when Range
38
+ range *range_pair(start)
39
+ else
40
+ []
41
+ end
42
+ end
43
+ alias_method :[], :slice
44
+
45
+ # Destructive slice. Returns specified range and removes other items.
46
+ # @overload slice(start, length)
47
+ def slice!(start, length = nil)
48
+ case start
49
+ when Range
50
+ trim *range_pair(start)
51
+ else
52
+ trim(start, start + length - 1)
53
+ end
54
+ to_a
55
+ end
56
+
57
+ # @param [Integer] start
58
+ # @param [Integer] finish
59
+ # @return [Array] items
60
+ def range(start, finish)
61
+ conn.lrange(key, start, finish) || []
62
+ end
63
+
64
+ # @param [Integer] start
65
+ # @param [Integer] finish
66
+ def trim(start, finish)
67
+ conn.ltrim(key, start, finish)
68
+ end
69
+
70
+ # @return [Integer] the number of items in the set
71
+ def length
72
+ conn.llen key
73
+ end
74
+ alias_method :size, :length
75
+
76
+ # Appends a single item. Chainable example:
77
+ # list << 'a' << 'b'
78
+ # @param [String] item
79
+ # A item to add
80
+ def push(item)
81
+ conn.rpush key, item
82
+ self
83
+ end
84
+ alias_method :<<, :push
85
+
86
+ # Removes the last item
87
+ # @return [String] the removed item
88
+ def pop
89
+ conn.rpop key
90
+ end
91
+
92
+ # Prepends a single item.
93
+ # @param [String] item
94
+ # A item to add
95
+ def unshift(item)
96
+ conn.lpush key, item
97
+ self
98
+ end
99
+
100
+ # Removes the first item
101
+ # @return [String] the removed item
102
+ def shift
103
+ conn.lpop key
104
+ end
105
+
106
+ # Removes the last item and prepends it to `target`
107
+ # @param [Redpear::Store::List] target
108
+ def pop_unshift(target)
109
+ conn.rpoplpush key, target.to_s
110
+ end
111
+
112
+ # Comparator
113
+ # @param [#to_a] other
114
+ # @return [Boolean] true if items match
115
+ def ==(other)
116
+ other.respond_to?(:to_a) && other.to_a == to_a
117
+ end
118
+
119
+ # Remove `item` from list
120
+ # @param [String] item
121
+ # @param [Integer] count
122
+ # The number of items to remove.
123
+ # When =0 - remove all occurences
124
+ # When >0 - remove the first `count` items
125
+ # When <0 - remove the last `count` items
126
+ def delete(item, count = 0)
127
+ conn.lrem key, count, item
128
+ end
129
+
130
+ # Insert `item` before `pivot`
131
+ # @param [String] pivot
132
+ # @param [String] item
133
+ def insert_before(pivot, item)
134
+ conn.linsert key, :before, pivot, item
135
+ end
136
+
137
+ # Insert `item` after `pivot`
138
+ # @param [String] pivot
139
+ # @param [String] item
140
+ def insert_after(pivot, item)
141
+ conn.linsert key, :after, pivot, item
142
+ end
143
+
144
+ end
@@ -0,0 +1,108 @@
1
+ class Redpear::Store::Lock < Redpear::Store::Base
2
+ class LockTimeout < ::StandardError; end
3
+
4
+ class << self
5
+
6
+ # Default lock options. Override via e.g.:
7
+ #
8
+ # Redpear::Store::Lock.default_options[:lock_timeout] = 5
9
+ #
10
+ # @return [Hash] default options
11
+ def default_options
12
+ @default_options ||= { :lock_timeout => 2, :wait_timeout => 2 }
13
+ end
14
+
15
+ end
16
+
17
+ # @return [Float] the current lock timestamp
18
+ def current
19
+ value.to_f
20
+ end
21
+
22
+ # @return [String] the current lock value
23
+ def value
24
+ conn.get key
25
+ end
26
+
27
+ # Creates a lock and yields a transaction. Example:
28
+ #
29
+ # sender = Redpear::Store::Hash.new "accounts:sender", connection
30
+ # recipient = Redpear::Store::Hash.new "accounts:recipient", connection
31
+ # lock = Redpear::Store::Lock.new "locks:transfer", connection
32
+ #
33
+ # lock.lock do
34
+ # sender.decrement 'balance', 100
35
+ # recipient.increment 'balance', 100
36
+ # end
37
+ #
38
+ # @param [Hash] options
39
+ # @option [Integer] lock_timeout
40
+ # Hold the lock for a maximum of `lock_timeout` seconds. Defaults to 2.
41
+ # @option [Integer] wait_timeout
42
+ # Wait for `wait_timeout` seconds to obtain a lock, before timing out. Defaults to 2.
43
+ # @yield [] processes the block within the lock
44
+ def lock(options = {})
45
+ options = self.class.default_options.merge(options)
46
+ result = nil
47
+ timestamp = nil
48
+ timeout = to_time(options[:wait_timeout])
49
+
50
+ while !timestamp && timeout > Time.now
51
+ timestamp = to_time(options[:lock_timeout]).to_f
52
+
53
+ if lock_obtained?(timestamp) || expired_lock_obtained?(timestamp)
54
+ result = yield
55
+ else
56
+ timestamp = nil # Unset
57
+ sleep 0.1
58
+ end
59
+ end
60
+
61
+ unless timestamp
62
+ raise LockTimeout, "Could not acquire lock on '#{key}'. Timed out."
63
+ end
64
+
65
+ result
66
+ ensure
67
+ purge! if timestamp && timestamp > Time.now.to_f
68
+ end
69
+
70
+ protected
71
+
72
+ # @param [Float] timestamp
73
+ # The timestamp to set for the lock
74
+ # @return [Boolean]
75
+ # True, if timestamp could be set and the lock was obtained
76
+ def lock_obtained?(timestamp)
77
+ conn.setnx key, timestamp
78
+ end
79
+
80
+ # @param [Float] timestamp
81
+ # The timestamp to set for the lock
82
+ # @return [Boolean]
83
+ # True, if key is set to an expired value and we can replace it
84
+ # with our `timestamp`
85
+ def expired_lock_obtained?(timestamp)
86
+ exists? && past?(value) && past?(conn.getset(key, timestamp))
87
+ end
88
+
89
+ # @return [Boolean] true, if timestamp has expired and is in the past
90
+ def past?(timestamp)
91
+ timestamp = timestamp.to_f if timestamp.is_a?(String)
92
+ timestamp < Time.now.to_f
93
+ end
94
+
95
+ private
96
+
97
+ def to_time(value)
98
+ case value
99
+ when Time
100
+ value
101
+ when Numeric
102
+ Time.now + value
103
+ else
104
+ Time.now + 5
105
+ end
106
+ end
107
+
108
+ end