record-cache 0.1.2 → 0.1.3
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.
- 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
|