seifertd-seifertd-cache-money 0.2.5.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 +205 -0
- data/TODO +17 -0
- data/UNSUPPORTED_FEATURES +14 -0
- data/config/environment.rb +6 -0
- data/config/memcache.yml +6 -0
- data/db/schema.rb +12 -0
- data/lib/cache_money.rb +54 -0
- data/lib/cash/accessor.rb +79 -0
- data/lib/cash/buffered.rb +126 -0
- data/lib/cash/config.rb +71 -0
- data/lib/cash/finders.rb +38 -0
- data/lib/cash/index.rb +207 -0
- data/lib/cash/local.rb +59 -0
- data/lib/cash/lock.rb +53 -0
- data/lib/cash/mock.rb +86 -0
- data/lib/cash/query/abstract.rb +162 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +51 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/request.rb +3 -0
- data/lib/cash/transactional.rb +42 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/write_through.rb +72 -0
- data/spec/cash/accessor_spec.rb +159 -0
- data/spec/cash/active_record_spec.rb +199 -0
- data/spec/cash/calculations_spec.rb +67 -0
- data/spec/cash/finders_spec.rb +355 -0
- data/spec/cash/lock_spec.rb +87 -0
- data/spec/cash/order_spec.rb +166 -0
- data/spec/cash/transactional_spec.rb +574 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +223 -0
- data/spec/spec_helper.rb +56 -0
- metadata +113 -0
@@ -0,0 +1,162 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Abstract
|
4
|
+
delegate :with_exclusive_scope, :get, :table_name, :indices, :find_from_ids_without_cache, :cache_key, :columns_hash, :to => :@active_record
|
5
|
+
|
6
|
+
def self.perform(*args)
|
7
|
+
new(*args).perform
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(active_record, options1, options2)
|
11
|
+
@active_record, @options1, @options2 = active_record, options1, options2 || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(find_options = {}, get_options = {})
|
15
|
+
if cache_config = cacheable?(@options1, @options2, find_options)
|
16
|
+
cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
|
17
|
+
|
18
|
+
misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
|
19
|
+
format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
|
20
|
+
else
|
21
|
+
uncacheable
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
DESC = /DESC/i
|
26
|
+
|
27
|
+
def order
|
28
|
+
@order ||= begin
|
29
|
+
if order_sql = @options1[:order] || @options2[:order]
|
30
|
+
matched, table_name, column_name, direction = *(ORDER.match(order_sql))
|
31
|
+
[column_name, direction =~ DESC ? :desc : :asc]
|
32
|
+
else
|
33
|
+
['id', :asc]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def limit
|
39
|
+
@limit ||= @options1[:limit] || @options2[:limit]
|
40
|
+
end
|
41
|
+
|
42
|
+
def offset
|
43
|
+
@offset ||= @options1[:offset] || @options2[:offset] || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def calculation?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def cacheable?(*optionss)
|
52
|
+
optionss.each { |options| return unless safe_options_for_cache?(options) }
|
53
|
+
partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
|
54
|
+
return if partial_indices.include?(nil)
|
55
|
+
attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
|
56
|
+
if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
|
57
|
+
if index.matches?(self)
|
58
|
+
[attribute_value_pairs, index]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def hit_or_miss(cache_keys, index, options)
|
64
|
+
misses, missed_keys = nil, nil
|
65
|
+
objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
|
66
|
+
misses = miss(missed_keys, @options1.merge(:limit => index.window))
|
67
|
+
serialize_objects(index, misses)
|
68
|
+
end
|
69
|
+
[misses, missed_keys, objects]
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache_keys(attribute_value_pairs)
|
73
|
+
attribute_value_pairs.flatten.join('/')
|
74
|
+
end
|
75
|
+
|
76
|
+
def safe_options_for_cache?(options)
|
77
|
+
return false unless options.kind_of?(Hash)
|
78
|
+
options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute_value_pairs_for_conditions(conditions)
|
82
|
+
case conditions
|
83
|
+
when Hash
|
84
|
+
conditions.to_a.collect { |key, value| [key.to_s, value] }
|
85
|
+
when String
|
86
|
+
parse_indices_from_condition(conditions)
|
87
|
+
when Array
|
88
|
+
parse_indices_from_condition(*conditions)
|
89
|
+
when NilClass
|
90
|
+
[]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
AND = /\s+AND\s+/i
|
95
|
+
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
96
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
97
|
+
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
|
98
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
99
|
+
|
100
|
+
def parse_indices_from_condition(conditions = '', *values)
|
101
|
+
values = values.dup
|
102
|
+
conditions.split(AND).inject([]) do |indices, condition|
|
103
|
+
matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
|
104
|
+
if matched
|
105
|
+
value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
|
106
|
+
indices << [column_name, value]
|
107
|
+
else
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def indexed_on?(attributes)
|
114
|
+
indices.detect { |index| index == attributes }
|
115
|
+
end
|
116
|
+
alias_method :index_for, :indexed_on?
|
117
|
+
|
118
|
+
def format_results(cache_keys, objects)
|
119
|
+
return objects if objects.blank?
|
120
|
+
|
121
|
+
objects = convert_to_array(cache_keys, objects)
|
122
|
+
objects = apply_limits_and_offsets(objects, @options1)
|
123
|
+
deserialize_objects(objects)
|
124
|
+
end
|
125
|
+
|
126
|
+
def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
|
127
|
+
missed_keys == cache_keys ? misses : objects
|
128
|
+
end
|
129
|
+
|
130
|
+
def serialize_objects(index, objects)
|
131
|
+
Array(objects).collect { |missed| index.serialize_object(missed) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def convert_to_array(cache_keys, object)
|
135
|
+
if object.kind_of?(Hash)
|
136
|
+
cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
|
137
|
+
else
|
138
|
+
Array(object)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def apply_limits_and_offsets(results, options)
|
143
|
+
results.slice((options[:offset] || 0), (options[:limit] || results.length))
|
144
|
+
end
|
145
|
+
|
146
|
+
def deserialize_objects(objects)
|
147
|
+
if objects.first.kind_of?(ActiveRecord::Base)
|
148
|
+
objects
|
149
|
+
else
|
150
|
+
cache_keys = objects.collect { |id| "id/#{id}" }
|
151
|
+
objects = get(cache_keys, &method(:find_from_keys))
|
152
|
+
convert_to_array(cache_keys, objects)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_from_keys(*missing_keys)
|
157
|
+
missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
|
158
|
+
find_from_ids_without_cache(missing_ids, {})
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Calculation < Abstract
|
4
|
+
delegate :calculate_without_cache, :incr, :to => :@active_record
|
5
|
+
|
6
|
+
def initialize(active_record, operation, column, options1, options2)
|
7
|
+
super(active_record, options1, options2)
|
8
|
+
@operation, @column = operation, column
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform
|
12
|
+
super({}, :raw => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculation?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def miss(_, __)
|
21
|
+
calculate_without_cache(@operation, @column, @options1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def uncacheable
|
25
|
+
calculate_without_cache(@operation, @column, @options1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_results(_, objects)
|
29
|
+
objects.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize_objects(_, objects)
|
33
|
+
objects.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def cacheable?(*optionss)
|
37
|
+
@column == :all && super(*optionss)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_keys(attribute_value_pairs)
|
41
|
+
"#{super(attribute_value_pairs)}/#{@operation}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class PrimaryKey < Abstract
|
4
|
+
def initialize(active_record, ids, options1, options2)
|
5
|
+
super(active_record, options1, options2)
|
6
|
+
@expects_array = ids.first.kind_of?(Array)
|
7
|
+
@original_ids = ids
|
8
|
+
@ids = ids.flatten.compact.uniq.collect do |object|
|
9
|
+
object.respond_to?(:quoted_id) ? object.quoted_id : object.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
return [] if @expects_array && @ids.empty?
|
15
|
+
raise ActiveRecord::RecordNotFound if @ids.empty?
|
16
|
+
|
17
|
+
super(:conditions => { :id => @ids.first })
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def deserialize_objects(objects)
|
22
|
+
convert_to_active_record_collection(super(objects))
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_keys(attribute_value_pairs)
|
26
|
+
@ids.collect { |id| "id/#{id}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def miss(missing_keys, options)
|
31
|
+
find_from_keys(*missing_keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
def uncacheable
|
35
|
+
find_from_ids_without_cache(@original_ids, @options1)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def convert_to_active_record_collection(objects)
|
40
|
+
case objects.size
|
41
|
+
when 0
|
42
|
+
raise ActiveRecord::RecordNotFound
|
43
|
+
when 1
|
44
|
+
@expects_array ? objects : objects.first
|
45
|
+
else
|
46
|
+
objects
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Select < Abstract
|
4
|
+
delegate :find_every_without_cache, :to => :@active_record
|
5
|
+
|
6
|
+
protected
|
7
|
+
def miss(_, miss_options)
|
8
|
+
find_every_without_cache(miss_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def uncacheable
|
12
|
+
find_every_without_cache(@options1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cash/request.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Cash
|
2
|
+
class Transactional
|
3
|
+
attr_reader :memcache
|
4
|
+
|
5
|
+
def initialize(memcache, lock)
|
6
|
+
@memcache, @cache = [memcache, memcache]
|
7
|
+
@lock = lock
|
8
|
+
end
|
9
|
+
|
10
|
+
def transaction
|
11
|
+
exception_was_raised = false
|
12
|
+
begin_transaction
|
13
|
+
result = yield
|
14
|
+
rescue Object => e
|
15
|
+
exception_was_raised = true
|
16
|
+
raise
|
17
|
+
ensure
|
18
|
+
begin
|
19
|
+
@cache.flush unless exception_was_raised
|
20
|
+
ensure
|
21
|
+
end_transaction
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing(method, *args, &block)
|
26
|
+
@cache.send(method, *args, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to?(method)
|
30
|
+
@cache.respond_to?(method)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def begin_transaction
|
35
|
+
@cache = Buffered.push(@cache, @lock)
|
36
|
+
end
|
37
|
+
|
38
|
+
def end_transaction
|
39
|
+
@cache = @cache.pop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
class Array
|
2
|
+
alias_method :count, :size
|
3
|
+
|
4
|
+
def to_hash
|
5
|
+
keys_and_values_without_nils = reject { |key, value| value.nil? }
|
6
|
+
shallow_flattened_keys_and_values_without_nils = keys_and_values_without_nils.inject([]) { |result, pair| result += pair }
|
7
|
+
Hash[*shallow_flattened_keys_and_values_without_nils]
|
8
|
+
end
|
9
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Cash
|
2
|
+
module WriteThrough
|
3
|
+
DEFAULT_TTL = 12.hours
|
4
|
+
|
5
|
+
def self.included(active_record_class)
|
6
|
+
active_record_class.class_eval do
|
7
|
+
include InstanceMethods
|
8
|
+
extend ClassMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
def self.included(active_record_class)
|
14
|
+
active_record_class.class_eval do
|
15
|
+
after_create :add_to_caches
|
16
|
+
after_update :update_caches
|
17
|
+
after_destroy :remove_from_caches
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def add_to_caches
|
22
|
+
InstanceMethods.unfold(self.class, :add_to_caches, self)
|
23
|
+
end
|
24
|
+
|
25
|
+
def update_caches
|
26
|
+
InstanceMethods.unfold(self.class, :update_caches, self)
|
27
|
+
end
|
28
|
+
|
29
|
+
def remove_from_caches
|
30
|
+
return if new_record?
|
31
|
+
InstanceMethods.unfold(self.class, :remove_from_caches, self)
|
32
|
+
end
|
33
|
+
|
34
|
+
def expire_caches
|
35
|
+
InstanceMethods.unfold(self.class, :expire_caches, self)
|
36
|
+
end
|
37
|
+
|
38
|
+
def shallow_clone
|
39
|
+
clone = self.class.new
|
40
|
+
clone.instance_variable_set("@attributes", instance_variable_get(:@attributes))
|
41
|
+
clone.instance_variable_set("@new_record", new_record?)
|
42
|
+
clone
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def self.unfold(klass, operation, object)
|
47
|
+
while klass < ActiveRecord::Base && klass.ancestors.include?(WriteThrough)
|
48
|
+
klass.send(operation, object)
|
49
|
+
klass = klass.superclass
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
def add_to_caches(object)
|
56
|
+
indices.each { |index| index.add(object) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def update_caches(object)
|
60
|
+
indices.each { |index| index.update(object) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def remove_from_caches(object)
|
64
|
+
indices.each { |index| index.remove(object) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def expire_caches(object)
|
68
|
+
indices.each { |index| index.delete(object) }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
|
+
|
3
|
+
module Cash
|
4
|
+
describe Accessor do
|
5
|
+
describe '#fetch' do
|
6
|
+
describe '#fetch("...")' do
|
7
|
+
describe 'when there is a cache miss' do
|
8
|
+
it 'returns nil' do
|
9
|
+
Story.fetch("yabba").should be_nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe 'when there is a cache hit' do
|
14
|
+
it 'returns the value of the cache' do
|
15
|
+
Story.set("yabba", "dabba")
|
16
|
+
Story.fetch("yabba").should == "dabba"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe '#fetch([...])', :shared => true do
|
22
|
+
describe 'when there is a total cache miss' do
|
23
|
+
it 'yields the keys to the block' do
|
24
|
+
Story.fetch(["yabba", "dabba"]) { |*missing_ids| ["doo", "doo"] }.should == {
|
25
|
+
"Story:1/yabba" => "doo",
|
26
|
+
"Story:1/dabba" => "doo"
|
27
|
+
}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'when there is a partial cache miss' do
|
32
|
+
it 'yields just the missing ids to the block' do
|
33
|
+
Story.set("yabba", "dabba")
|
34
|
+
Story.fetch(["yabba", "dabba"]) { |*missing_ids| "doo" }.should == {
|
35
|
+
"Story:1/yabba" => "dabba",
|
36
|
+
"Story:1/dabba" => "doo"
|
37
|
+
}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#get' do
|
44
|
+
describe '#get("...")' do
|
45
|
+
describe 'when there is a cache miss' do
|
46
|
+
it 'returns the value of the block' do
|
47
|
+
Story.get("yabba") { "dabba" }.should == "dabba"
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'adds to the cache' do
|
51
|
+
Story.get("yabba") { "dabba" }
|
52
|
+
Story.get("yabba").should == "dabba"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe 'when there is a cache hit' do
|
57
|
+
before do
|
58
|
+
Story.set("yabba", "dabba")
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'returns the value of the cache' do
|
62
|
+
Story.get("yabba") { "doo" }.should == "dabba"
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'does nothing to the cache' do
|
66
|
+
Story.get("yabba") { "doo" }
|
67
|
+
Story.get("yabba").should == "dabba"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe '#get([...])' do
|
73
|
+
it_should_behave_like "#fetch([...])"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe '#incr' do
|
78
|
+
describe 'when there is a cache hit' do
|
79
|
+
before do
|
80
|
+
Story.set("count", 0)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'increments the value of the cache' do
|
84
|
+
Story.incr("count", 2)
|
85
|
+
Story.get("count", :raw => true).should =~ /2/
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns the new cache value' do
|
89
|
+
Story.incr("count", 2).should == 2
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'when there is a cache miss' do
|
94
|
+
it 'initializes the value of the cache to the value of the block' do
|
95
|
+
Story.incr("count", 1) { 5 }
|
96
|
+
Story.get("count", :raw => true).should =~ /5/
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'returns the new cache value' do
|
100
|
+
Story.incr("count", 1) { 2 }.should == 2
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#add' do
|
106
|
+
describe 'when the value already exists' do
|
107
|
+
it 'yields to the block' do
|
108
|
+
Story.set("count", 1)
|
109
|
+
Story.add("count", 1) { "yield me" }.should == "yield me"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe 'when the value does not already exist' do
|
114
|
+
it 'adds the key to the cache' do
|
115
|
+
Story.add("count", 1)
|
116
|
+
Story.get("count").should == 1
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#decr' do
|
122
|
+
describe 'when there is a cache hit' do
|
123
|
+
before do
|
124
|
+
Story.incr("count", 1) { 10 }
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'decrements the value of the cache' do
|
128
|
+
Story.decr("count", 2)
|
129
|
+
Story.get("count", :raw => true).should =~ /8/
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'returns the new cache value' do
|
133
|
+
Story.decr("count", 2).should == 8
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe 'when there is a cache miss' do
|
138
|
+
it 'initializes the value of the cache to the value of the block' do
|
139
|
+
Story.decr("count", 1) { 5 }
|
140
|
+
Story.get("count", :raw => true).should =~ /5/
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'returns the new cache value' do
|
144
|
+
Story.decr("count", 1) { 2 }.should == 2
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '#cache_key' do
|
150
|
+
it 'uses the version number' do
|
151
|
+
Story.version 1
|
152
|
+
Story.cache_key("foo").should == "Story:1/foo"
|
153
|
+
|
154
|
+
Story.version 2
|
155
|
+
Story.cache_key("foo").should == "Story:2/foo"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|