record-cache 0.1.2 → 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/lib/record_cache.rb +2 -1
- data/lib/record_cache/base.rb +63 -22
- data/lib/record_cache/datastore/active_record.rb +5 -3
- data/lib/record_cache/datastore/active_record_30.rb +95 -38
- data/lib/record_cache/datastore/active_record_31.rb +157 -54
- data/lib/record_cache/datastore/active_record_32.rb +444 -0
- data/lib/record_cache/dispatcher.rb +47 -47
- data/lib/record_cache/multi_read.rb +14 -1
- data/lib/record_cache/query.rb +36 -25
- data/lib/record_cache/statistics.rb +5 -5
- data/lib/record_cache/strategy/base.rb +49 -19
- data/lib/record_cache/strategy/full_table_cache.rb +81 -0
- data/lib/record_cache/strategy/index_cache.rb +38 -36
- data/lib/record_cache/strategy/unique_index_cache.rb +130 -0
- data/lib/record_cache/strategy/util.rb +12 -12
- data/lib/record_cache/test/resettable_version_store.rb +2 -9
- data/lib/record_cache/version.rb +1 -1
- data/lib/record_cache/version_store.rb +23 -16
- data/spec/db/schema.rb +12 -0
- data/spec/db/seeds.rb +10 -0
- data/spec/lib/active_record/visitor_spec.rb +22 -0
- data/spec/lib/base_spec.rb +21 -0
- data/spec/lib/dispatcher_spec.rb +24 -46
- data/spec/lib/multi_read_spec.rb +6 -6
- data/spec/lib/query_spec.rb +43 -43
- data/spec/lib/statistics_spec.rb +28 -28
- data/spec/lib/strategy/base_spec.rb +98 -87
- data/spec/lib/strategy/full_table_cache_spec.rb +68 -0
- data/spec/lib/strategy/index_cache_spec.rb +112 -69
- data/spec/lib/strategy/query_cache_spec.rb +83 -0
- data/spec/lib/strategy/unique_index_on_id_cache_spec.rb +317 -0
- data/spec/lib/strategy/unique_index_on_string_cache_spec.rb +168 -0
- data/spec/lib/strategy/util_spec.rb +67 -49
- data/spec/lib/version_store_spec.rb +22 -41
- data/spec/models/address.rb +9 -0
- data/spec/models/apple.rb +1 -1
- data/spec/models/banana.rb +21 -2
- data/spec/models/language.rb +5 -0
- data/spec/models/person.rb +1 -1
- data/spec/models/store.rb +2 -1
- data/spec/spec_helper.rb +7 -4
- data/spec/support/after_commit.rb +2 -0
- data/spec/support/matchers/hit_cache_matcher.rb +10 -6
- data/spec/support/matchers/log.rb +45 -0
- data/spec/support/matchers/miss_cache_matcher.rb +10 -6
- data/spec/support/matchers/use_cache_matcher.rb +10 -6
- metadata +156 -161
- data/lib/record_cache/strategy/id_cache.rb +0 -93
- data/lib/record_cache/strategy/request_cache.rb +0 -49
- data/spec/lib/strategy/id_cache_spec.rb +0 -168
- data/spec/lib/strategy/request_cache_spec.rb +0 -85
@@ -0,0 +1,130 @@
|
|
1
|
+
module RecordCache
|
2
|
+
module Strategy
|
3
|
+
class UniqueIndexCache < Base
|
4
|
+
|
5
|
+
# All attributes with a unique index for the given model
|
6
|
+
def self.attributes(base)
|
7
|
+
(@attributes ||= {})[base.name] ||= []
|
8
|
+
end
|
9
|
+
|
10
|
+
# parse the options and return (an array of) instances of this strategy
|
11
|
+
def self.parse(base, record_store, options)
|
12
|
+
return nil unless base.table_exists?
|
13
|
+
|
14
|
+
attributes = [options[:unique_index]].flatten.compact
|
15
|
+
# add unique index for :id by default
|
16
|
+
attributes << :id if base.columns_hash['id'] unless base.record_cache[:id]
|
17
|
+
attributes.uniq! # in development mode, do not keep adding 'id' to the list of unique index attributes
|
18
|
+
return nil if attributes.empty?
|
19
|
+
attributes.map do |attribute|
|
20
|
+
type = base.columns_hash[attribute.to_s].try(:type)
|
21
|
+
raise "No column found for unique index '#{index}' on #{base.name}." unless type
|
22
|
+
raise "Incorrect type (expected string or integer, found #{type}) for unique index '#{attribute}' on #{base.name}." unless type == :string || type == :integer
|
23
|
+
UniqueIndexCache.new(base, attribute, record_store, options, type)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize(base, attribute, record_store, options, type)
|
28
|
+
super(base, attribute, record_store, options)
|
29
|
+
# remember the attributes with a unique index
|
30
|
+
UniqueIndexCache.attributes(base) << attribute
|
31
|
+
# for unique indexes that are not on the :id column, use key: rc/<key or model name>/<attribute>:
|
32
|
+
@cache_key_prefix << "#{attribute}:" unless attribute == :id
|
33
|
+
@type = type
|
34
|
+
end
|
35
|
+
|
36
|
+
# Can the cache retrieve the records based on this query?
|
37
|
+
def cacheable?(query)
|
38
|
+
values = query.where_values(@attribute, @type)
|
39
|
+
values && (query.limit.nil? || (query.limit == 1 && values.size == 1))
|
40
|
+
end
|
41
|
+
|
42
|
+
# Update the version store and the record store
|
43
|
+
def record_change(record, action)
|
44
|
+
key = cache_key(record.send(@attribute))
|
45
|
+
if action == :destroy
|
46
|
+
version_store.delete(key)
|
47
|
+
else
|
48
|
+
# update the version store and add the record to the cache
|
49
|
+
new_version = version_store.renew(key, version_opts)
|
50
|
+
record_store.write(versioned_key(key, new_version), Util.serialize(record))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
protected
|
55
|
+
|
56
|
+
# retrieve the record(s) with the given id(s) as an array
|
57
|
+
def fetch_records(query)
|
58
|
+
ids = query.where_values(@attribute, @type)
|
59
|
+
query.wheres.delete(@attribute) # make sure CacheCase.filter! does not see this where anymore
|
60
|
+
id_to_key_map = ids.inject({}){|h,id| h[id] = cache_key(id); h }
|
61
|
+
# retrieve the current version of the records
|
62
|
+
current_versions = version_store.current_multi(id_to_key_map)
|
63
|
+
# get the keys for the records for which a current version was found
|
64
|
+
id_to_version_key_map = Hash[id_to_key_map.map{ |id, key| current_versions[id] ? [id, versioned_key(key, current_versions[id])] : nil }.compact]
|
65
|
+
# retrieve the records from the cache
|
66
|
+
records = id_to_version_key_map.size > 0 ? from_cache(id_to_version_key_map) : []
|
67
|
+
# query the records with missing ids
|
68
|
+
id_to_key_map.except!(*records.map(&@attribute))
|
69
|
+
# logging (only in debug mode!) and statistics
|
70
|
+
log_id_cache_hit(ids, id_to_key_map.keys) if RecordCache::Base.logger.debug?
|
71
|
+
statistics.add(ids.size, records.size) if statistics.active?
|
72
|
+
# retrieve records from DB in case there are some missing ids
|
73
|
+
records += from_db(id_to_key_map, id_to_version_key_map) if id_to_key_map.size > 0
|
74
|
+
# return the array
|
75
|
+
records
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# ---------------------------- Querying ------------------------------------
|
81
|
+
|
82
|
+
# retrieve the records from the cache with the given keys
|
83
|
+
def from_cache(id_to_versioned_key_map)
|
84
|
+
records = record_store.read_multi(*(id_to_versioned_key_map.values)).values.compact
|
85
|
+
records.map do |record|
|
86
|
+
record = Util.deserialize(record)
|
87
|
+
record.becomes(self.instance_variable_get('@base')) unless record.class == self.instance_variable_get('@base')
|
88
|
+
record
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# retrieve the records with the given ids from the database
|
93
|
+
def from_db(id_to_key_map, id_to_version_key_map)
|
94
|
+
# skip record cache itself
|
95
|
+
RecordCache::Base.without_record_cache do
|
96
|
+
# set version store in multi-mode
|
97
|
+
RecordCache::Base.version_store.multi do
|
98
|
+
# set record store in multi-mode
|
99
|
+
record_store.multi do
|
100
|
+
# retrieve the records from the database
|
101
|
+
records = @base.where(@attribute => id_to_key_map.keys).to_a
|
102
|
+
records.each do |record|
|
103
|
+
versioned_key = id_to_version_key_map[record.send(@attribute)]
|
104
|
+
unless versioned_key
|
105
|
+
# renew the key in the version store in case it was missing
|
106
|
+
key = id_to_key_map[record.send(@attribute)]
|
107
|
+
versioned_key = versioned_key(key, version_store.renew(key, version_opts))
|
108
|
+
end
|
109
|
+
# store the record based on the versioned key
|
110
|
+
record_store.write(versioned_key, Util.serialize(record))
|
111
|
+
end
|
112
|
+
records
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
# ------------------------- Utility methods ----------------------------
|
119
|
+
|
120
|
+
# log cache hit/miss to debug log
|
121
|
+
def log_id_cache_hit(ids, missing_ids)
|
122
|
+
hit = missing_ids.empty? ? "hit" : ids.size == missing_ids.size ? "miss" : "partial hit"
|
123
|
+
missing = missing_ids.empty? || ids.size == missing_ids.size ? "" : ": missing #{missing_ids.inspect}"
|
124
|
+
msg = "UniqueIndexCache on '#{@base.name}.#{@attribute}' #{hit} for ids #{ids.size == 1 ? ids.first.inspect : ids.inspect}#{missing}"
|
125
|
+
RecordCache::Base.logger.debug{ msg }
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -11,17 +11,17 @@ module RecordCache
|
|
11
11
|
# creates a shallow clone with a version and without associations
|
12
12
|
def serialize(record)
|
13
13
|
{CLASS_KEY => record.class.name,
|
14
|
-
ATTRIBUTES_KEY => record.instance_variable_get(:@attributes)}
|
14
|
+
ATTRIBUTES_KEY => record.instance_variable_get(:@attributes).dup}
|
15
15
|
end
|
16
16
|
|
17
17
|
# deserialize a cached record
|
18
18
|
def deserialize(serialized)
|
19
|
-
record = serialized[CLASS_KEY].constantize.
|
19
|
+
record = serialized[CLASS_KEY].constantize.allocate
|
20
20
|
attributes = serialized[ATTRIBUTES_KEY]
|
21
|
-
record.
|
22
|
-
|
23
|
-
|
24
|
-
record.
|
21
|
+
record.class.serialized_attributes.keys.each do |attribute|
|
22
|
+
attributes[attribute] = attributes[attribute].unserialize if attributes[attribute].respond_to?(:unserialize)
|
23
|
+
end
|
24
|
+
record.init_with('attributes' => attributes)
|
25
25
|
record
|
26
26
|
end
|
27
27
|
|
@@ -81,23 +81,23 @@ module RecordCache
|
|
81
81
|
# Proc.new{ |x,y| { ([(COLLATER.collate(x.name) || NIL_COMES_FIRST), (y.updated_at || NIL_COMES_FIRST)] <=> [COLLATER.collate(y.name), x.updated_at]) || 1 }
|
82
82
|
eval("Proc.new{ |x,y| (#{sort[0]} <=> #{sort[1]}) || 1 }")
|
83
83
|
end
|
84
|
-
|
84
|
+
|
85
85
|
# If +x.nil?+ this class will return -1 for +x <=> y+
|
86
86
|
NIL_COMES_FIRST = ((class NilComesFirst; def <=>(y); -1; end; end); NilComesFirst.new)
|
87
|
-
|
87
|
+
|
88
88
|
# StringCollator uses the Rails transliterate method for collation
|
89
89
|
module Collator
|
90
90
|
@collated = []
|
91
|
-
|
91
|
+
|
92
92
|
def self.clear
|
93
93
|
@collated.each { |string| string.send(:remove_instance_variable, :@rc_collated) }
|
94
94
|
@collated.clear
|
95
95
|
end
|
96
|
-
|
96
|
+
|
97
97
|
def self.collate(string)
|
98
98
|
collated = string.instance_variable_get(:@rc_collated)
|
99
99
|
return collated if collated
|
100
|
-
normalized = ActiveSupport::Multibyte::Unicode.normalize(ActiveSupport::Multibyte::Unicode.tidy_bytes(string), :c).mb_chars
|
100
|
+
normalized = ActiveSupport::Multibyte::Unicode.normalize(ActiveSupport::Multibyte::Unicode.tidy_bytes(string || ''), :c).mb_chars
|
101
101
|
collated = I18n.transliterate(normalized).downcase.mb_chars
|
102
102
|
# transliterate will replace ignored/unknown chars with ? the following line replaces ? with the original character
|
103
103
|
collated.chars.each_with_index{ |c, i| collated[i] = normalized[i] if c == '?' } if collated.index('?')
|
@@ -108,7 +108,7 @@ module RecordCache
|
|
108
108
|
end
|
109
109
|
end
|
110
110
|
end
|
111
|
-
|
111
|
+
|
112
112
|
end
|
113
113
|
end
|
114
114
|
end
|
@@ -11,7 +11,6 @@ module RecordCache
|
|
11
11
|
base.extend ClassMethods
|
12
12
|
base.send(:include, InstanceMethods)
|
13
13
|
base.instance_eval do
|
14
|
-
alias_method_chain :increment, :reset
|
15
14
|
alias_method_chain :renew, :reset
|
16
15
|
end
|
17
16
|
end
|
@@ -21,18 +20,12 @@ module RecordCache
|
|
21
20
|
|
22
21
|
module InstanceMethods
|
23
22
|
|
24
|
-
def
|
23
|
+
def renew_with_reset(key, opts = {})
|
25
24
|
updated_version_keys << key
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def renew_with_reset(key)
|
30
|
-
updated_version_keys << key
|
31
|
-
renew_without_reset(key)
|
25
|
+
renew_without_reset(key, opts)
|
32
26
|
end
|
33
27
|
|
34
28
|
def reset!
|
35
|
-
RecordCache::Strategy::RequestCache.clear
|
36
29
|
updated_version_keys.each { |key| delete(key) }
|
37
30
|
updated_version_keys.clear
|
38
31
|
end
|
data/lib/record_cache/version.rb
CHANGED
@@ -3,7 +3,9 @@ module RecordCache
|
|
3
3
|
attr_accessor :store
|
4
4
|
|
5
5
|
def initialize(store)
|
6
|
-
|
6
|
+
[:write, :read, :read_multi, :delete].each do |method|
|
7
|
+
raise "Store #{store.class.name} must respond to #{method}" unless store.respond_to?(method)
|
8
|
+
end
|
7
9
|
@store = store
|
8
10
|
end
|
9
11
|
|
@@ -22,31 +24,36 @@ module RecordCache
|
|
22
24
|
Hash[id_key_map.map{ |id, key| [id, current_versions[key]] }]
|
23
25
|
end
|
24
26
|
|
25
|
-
#
|
26
|
-
|
27
|
-
def renew(key)
|
27
|
+
# Call this method to reset the key to a new (unique) version
|
28
|
+
def renew(key, options = {})
|
28
29
|
new_version = (Time.current.to_f * 10000).to_i
|
29
|
-
|
30
|
-
|
30
|
+
seconds = options[:ttl] ? options[:ttl] + (rand(options[:ttl] / 2) * [1, -1].sample) : nil
|
31
|
+
@store.write(key, new_version, {:expires_in => seconds})
|
32
|
+
RecordCache::Base.logger.debug{ "Version Store: renew #{key}: nil => #{new_version}" }
|
31
33
|
new_version
|
32
34
|
end
|
33
35
|
|
34
|
-
#
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
if
|
39
|
-
|
36
|
+
# perform several actions on the version store in one go
|
37
|
+
# Dalli: Turn on quiet aka noreply support. All relevant operations within this block will be effectively pipelined using 'quiet' operations where possible.
|
38
|
+
# Currently supports the set, add, replace and delete operations for Dalli cache.
|
39
|
+
def multi(&block)
|
40
|
+
if @store.respond_to?(:multi)
|
41
|
+
@store.multi(&block)
|
40
42
|
else
|
41
|
-
|
43
|
+
yield
|
42
44
|
end
|
43
|
-
version
|
44
45
|
end
|
45
|
-
|
46
|
+
|
47
|
+
# @deprecated: use renew instead
|
48
|
+
def increment(key)
|
49
|
+
RecordCache::Base.logger.debug{ "increment is deprecated, use renew instead. Called from: #{caller[0]}" }
|
50
|
+
renew(key)
|
51
|
+
end
|
52
|
+
|
46
53
|
# Delete key from the version store (records cached in the Record Store belonging to this key will become unreachable)
|
47
54
|
def delete(key)
|
48
55
|
deleted = @store.delete(key)
|
49
|
-
RecordCache::Base.logger.debug
|
56
|
+
RecordCache::Base.logger.debug{ "Version Store: deleted #{key}" }
|
50
57
|
deleted
|
51
58
|
end
|
52
59
|
|
data/spec/db/schema.rb
CHANGED
@@ -39,4 +39,16 @@ ActiveRecord::Schema.define :version => 0 do
|
|
39
39
|
t.integer :person_id
|
40
40
|
end
|
41
41
|
|
42
|
+
create_table :addresses, :force => true do |t|
|
43
|
+
t.integer :id
|
44
|
+
t.string :name
|
45
|
+
t.integer :store_id
|
46
|
+
t.string :location
|
47
|
+
end
|
48
|
+
|
49
|
+
create_table :languages, :force => true do |t|
|
50
|
+
t.string :name
|
51
|
+
t.string :locale
|
52
|
+
end
|
53
|
+
|
42
54
|
end
|
data/spec/db/seeds.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
# coding: utf-8
|
1
2
|
ActiveRecord::Schema.define :version => 1 do
|
2
3
|
|
3
4
|
# Make sure that at the beginning of the tests, NOTHING is known to Record Cache
|
@@ -11,6 +12,10 @@ ActiveRecord::Schema.define :version => 1 do
|
|
11
12
|
@blue_fruits = Store.create!(:name => "Blue Fruits", :owner => @blue)
|
12
13
|
@cris_bananas = Store.create!(:name => "Chris Bananas", :owner => @cris)
|
13
14
|
|
15
|
+
@adam_apples_address = Address.create!(:name => "101 1st street", :store => @adam_apples)
|
16
|
+
@blue_fruits_address = Address.create!(:name => "102 1st street", :store => @blue_fruits)
|
17
|
+
@cris_bananas_address = Address.create!(:name => "103 1st street", :store => @cris_bananas, :location => {latitue: 27.175015, longitude: 78.042155, dms_lat: %(27° 10' 30.0540" N), dms_long: %(78° 2' 31.7580" E)})
|
18
|
+
|
14
19
|
@fry = Person.create!(:name => "Fry", :birthday => Date.civil(1985,01,20), :height => 1.69)
|
15
20
|
@chase = Person.create!(:name => "Chase", :birthday => Date.civil(1970,07,03), :height => 1.91)
|
16
21
|
@penny = Person.create!(:name => "Penny", :birthday => Date.civil(1958,04,16), :height => 1.61)
|
@@ -36,5 +41,10 @@ ActiveRecord::Schema.define :version => 1 do
|
|
36
41
|
Pear.create!(:name => "Blue Pear 3", :store => @blue_fruits, :person => @chase)
|
37
42
|
Pear.create!(:name => "Blue Pear 4", :store => @blue_fruits, :person => @chase)
|
38
43
|
|
44
|
+
Language.create!(:name => "English (US)", :locale => "en-US")
|
45
|
+
Language.create!(:name => "English (GB)", :locale => "en-GB")
|
46
|
+
Language.create!(:name => "Nederlands (NL)", :locale => "du-NL")
|
47
|
+
Language.create!(:name => "Magyar", :locale => "hu")
|
48
|
+
|
39
49
|
RecordCache::Base.enable
|
40
50
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe 'ActiveRecord Visitor' do
|
5
|
+
|
6
|
+
def find_visit_methods(visitor_class)
|
7
|
+
(visitor_class.instance_methods + visitor_class.private_instance_methods).select{ |method| method.to_s =~ /^visit_Arel_/ }.sort.uniq
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should implement all visitor methods' do
|
11
|
+
all_visit_methods = find_visit_methods(Arel::Visitors::ToSql)
|
12
|
+
rc_visit_methods = find_visit_methods(RecordCache::Arel::QueryVisitor)
|
13
|
+
expect(all_visit_methods - rc_visit_methods).to be_empty
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should not implement old visitor methods' do
|
17
|
+
all_visit_methods = find_visit_methods(Arel::Visitors::ToSql)
|
18
|
+
rc_visit_methods = find_visit_methods(RecordCache::Arel::QueryVisitor)
|
19
|
+
expect(rc_visit_methods - all_visit_methods).to be_empty
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe RecordCache::Base do
|
5
|
+
|
6
|
+
it "should run a block in enabled mode" do
|
7
|
+
RecordCache::Base.disable!
|
8
|
+
RecordCache::Base.enabled do
|
9
|
+
expect(RecordCache::Base.status).to eq(RecordCache::ENABLED)
|
10
|
+
end
|
11
|
+
expect(RecordCache::Base.status).to eq(RecordCache::DISABLED)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should be possible to provide a different logger" do
|
15
|
+
custom_logger = Logger.new(STDOUT)
|
16
|
+
RecordCache::Base.logger = custom_logger
|
17
|
+
expect(RecordCache::Base.logger).to eq(custom_logger)
|
18
|
+
RecordCache::Base.logger = nil
|
19
|
+
expect(RecordCache::Base.logger).to eq(::ActiveRecord::Base.logger)
|
20
|
+
end
|
21
|
+
end
|
data/spec/lib/dispatcher_spec.rb
CHANGED
@@ -4,55 +4,45 @@ describe RecordCache::Dispatcher do
|
|
4
4
|
before(:each) do
|
5
5
|
@apple_dispatcher = Apple.record_cache
|
6
6
|
end
|
7
|
-
|
8
|
-
it "should raise an error when the same index is added twice" do
|
9
|
-
lambda { @apple_dispatcher.register(:store_id, RecordCache::Strategy::IdCache, nil, {}) }.should raise_error("Multiple record cache definitions found for 'store_id' on Apple")
|
10
|
-
end
|
11
|
-
|
12
|
-
it "should return the Cache for the requested strategy" do
|
13
|
-
@apple_dispatcher[:id].class.should == RecordCache::Strategy::IdCache
|
14
|
-
@apple_dispatcher[:store_id].class.should == RecordCache::Strategy::IndexCache
|
15
|
-
end
|
16
7
|
|
17
|
-
it "should return
|
18
|
-
|
8
|
+
it "should return the (ordered) strategy classes" do
|
9
|
+
expect(RecordCache::Dispatcher.strategy_classes).to eq([RecordCache::Strategy::UniqueIndexCache, RecordCache::Strategy::FullTableCache, RecordCache::Strategy::IndexCache])
|
19
10
|
end
|
20
11
|
|
21
|
-
it "should
|
22
|
-
|
23
|
-
|
24
|
-
|
12
|
+
it "should be able to register a new strategy" do
|
13
|
+
RecordCache::Dispatcher.strategy_classes << Integer
|
14
|
+
expect(RecordCache::Dispatcher.strategy_classes).to include(Integer)
|
15
|
+
RecordCache::Dispatcher.strategy_classes.delete(Integer)
|
25
16
|
end
|
26
17
|
|
27
|
-
context "
|
28
|
-
it "should
|
29
|
-
|
30
|
-
mock(@apple_dispatcher[:request_cache]).fetch(query)
|
31
|
-
@apple_dispatcher.fetch(query)
|
32
|
-
end
|
33
|
-
|
34
|
-
it "should delegate fetch to the first cacheable strategy if Request Cache is not present" do
|
35
|
-
query = RecordCache::Query.new
|
36
|
-
banana_dispatcher = Banana.record_cache
|
37
|
-
banana_dispatcher[:request_cache].should == nil
|
38
|
-
mock(banana_dispatcher).first_cacheable_strategy(query) { mock(Object.new).fetch(query) }
|
39
|
-
banana_dispatcher.fetch(query)
|
18
|
+
context "parse" do
|
19
|
+
it "should raise an error when the same index is added twice" do
|
20
|
+
expect{ Apple.cache_records(:index => :store_id) }.to raise_error("Multiple record cache definitions found for 'store_id' on Apple")
|
40
21
|
end
|
41
22
|
end
|
42
23
|
|
24
|
+
it "should return the Cache for the requested strategy" do
|
25
|
+
expect(@apple_dispatcher[:id].class).to eq(RecordCache::Strategy::UniqueIndexCache)
|
26
|
+
expect(@apple_dispatcher[:store_id].class).to eq(RecordCache::Strategy::IndexCache)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should return nil for unknown requested strategies" do
|
30
|
+
expect(@apple_dispatcher[:unknown]).to be_nil
|
31
|
+
end
|
32
|
+
|
43
33
|
context "record_change" do
|
44
34
|
it "should dispatch record_change to all strategies" do
|
45
35
|
apple = Apple.first
|
46
36
|
[:id, :store_id, :person_id].each do |strategy|
|
47
|
-
|
37
|
+
expect(@apple_dispatcher[strategy]).to receive(:record_change).with(apple, :create)
|
48
38
|
end
|
49
39
|
@apple_dispatcher.record_change(apple, :create)
|
50
40
|
end
|
51
41
|
|
52
42
|
it "should not dispatch record_change for updates without changes" do
|
53
43
|
apple = Apple.first
|
54
|
-
[:
|
55
|
-
|
44
|
+
[:id, :store_id, :person_id].each do |strategy|
|
45
|
+
expect(@apple_dispatcher[strategy]).to_not receive(:record_change)
|
56
46
|
end
|
57
47
|
@apple_dispatcher.record_change(apple, :update)
|
58
48
|
end
|
@@ -60,27 +50,15 @@ describe RecordCache::Dispatcher do
|
|
60
50
|
|
61
51
|
context "invalidate" do
|
62
52
|
it "should default to the :id strategy" do
|
63
|
-
|
53
|
+
expect(@apple_dispatcher[:id]).to receive(:invalidate).with(15)
|
64
54
|
@apple_dispatcher.invalidate(15)
|
65
55
|
end
|
66
56
|
|
67
57
|
it "should delegate to given strategy" do
|
68
|
-
|
69
|
-
|
58
|
+
expect(@apple_dispatcher[:id]).to receive(:invalidate).with(15)
|
59
|
+
expect(@apple_dispatcher[:store_id]).to receive(:invalidate).with(31)
|
70
60
|
@apple_dispatcher.invalidate(:id, 15)
|
71
61
|
@apple_dispatcher.invalidate(:store_id, 31)
|
72
62
|
end
|
73
|
-
|
74
|
-
it "should invalidate the request cache" do
|
75
|
-
store_dispatcher = Store.record_cache
|
76
|
-
mock(store_dispatcher[:request_cache]).invalidate(15)
|
77
|
-
store_dispatcher.invalidate(:id, 15)
|
78
|
-
end
|
79
|
-
|
80
|
-
it "should even invalidate the request cache if the given strategy is not known" do
|
81
|
-
store_dispatcher = Store.record_cache
|
82
|
-
mock(store_dispatcher[:request_cache]).invalidate(31)
|
83
|
-
store_dispatcher.invalidate(:unknown_id, 31)
|
84
|
-
end
|
85
63
|
end
|
86
64
|
end
|