garner 0.4.5 → 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +35 -0
- data/.travis.yml +13 -0
- data/CHANGELOG.md +130 -0
- data/CONTRIBUTING.md +118 -0
- data/Gemfile +3 -0
- data/README.md +1 -0
- data/Rakefile +39 -0
- data/UPGRADING.md +118 -0
- data/garner.gemspec +44 -0
- data/lib/garner.rb +21 -21
- data/lib/garner/cache.rb +13 -6
- data/lib/garner/cache/binding.rb +6 -14
- data/lib/garner/cache/context.rb +11 -12
- data/lib/garner/cache/identity.rb +1 -1
- data/lib/garner/config.rb +12 -7
- data/lib/garner/mixins/active_record.rb +3 -3
- data/lib/garner/mixins/active_record/base.rb +2 -2
- data/lib/garner/mixins/mongoid.rb +4 -4
- data/lib/garner/mixins/mongoid/document.rb +8 -12
- data/lib/garner/mixins/mongoid/identity.rb +5 -6
- data/lib/garner/mixins/rack.rb +1 -2
- data/lib/garner/strategies/binding/invalidation/base.rb +2 -4
- data/lib/garner/strategies/binding/invalidation/binding_index.rb +1 -3
- data/lib/garner/strategies/binding/invalidation/touch.rb +0 -2
- data/lib/garner/strategies/binding/key/base.rb +1 -3
- data/lib/garner/strategies/binding/key/binding_index.rb +3 -4
- data/lib/garner/strategies/binding/key/cache_key.rb +0 -2
- data/lib/garner/strategies/binding/key/safe_cache_key.rb +2 -3
- data/lib/garner/strategies/context/key/base.rb +1 -3
- data/lib/garner/strategies/context/key/caller.rb +9 -12
- data/lib/garner/strategies/context/key/jsonp.rb +3 -6
- data/lib/garner/strategies/context/key/request_get.rb +2 -4
- data/lib/garner/strategies/context/key/request_path.rb +1 -3
- data/lib/garner/strategies/context/key/request_post.rb +2 -4
- data/lib/garner/version.rb +1 -1
- data/spec/garner/cache/context_spec.rb +38 -0
- data/spec/garner/cache/identity_spec.rb +68 -0
- data/spec/garner/cache_spec.rb +49 -0
- data/spec/garner/config_spec.rb +17 -0
- data/spec/garner/mixins/mongoid/document_spec.rb +80 -0
- data/spec/garner/mixins/mongoid/identity_spec.rb +140 -0
- data/spec/garner/mixins/rack_spec.rb +48 -0
- data/spec/garner/strategies/binding/invalidation/binding_index_spec.rb +14 -0
- data/spec/garner/strategies/binding/invalidation/touch_spec.rb +23 -0
- data/spec/garner/strategies/binding/key/binding_index_spec.rb +245 -0
- data/spec/garner/strategies/binding/key/cache_key_spec.rb +29 -0
- data/spec/garner/strategies/binding/key/safe_cache_key_spec.rb +61 -0
- data/spec/garner/strategies/context/key/caller_spec.rb +106 -0
- data/spec/garner/strategies/context/key/jsonp_spec.rb +22 -0
- data/spec/garner/strategies/context/key/request_get_spec.rb +33 -0
- data/spec/garner/strategies/context/key/request_path_spec.rb +28 -0
- data/spec/garner/strategies/context/key/request_post_spec.rb +34 -0
- data/spec/garner/version_spec.rb +11 -0
- data/spec/integration/active_record_spec.rb +43 -0
- data/spec/integration/grape_spec.rb +33 -0
- data/spec/integration/mongoid_spec.rb +355 -0
- data/spec/integration/rack_spec.rb +77 -0
- data/spec/integration/sinatra_spec.rb +29 -0
- data/spec/performance/strategy_benchmark.rb +59 -0
- data/spec/performance/support/benchmark_context.rb +31 -0
- data/spec/performance/support/benchmark_context_wrapper.rb +67 -0
- data/spec/shared/binding_invalidation_strategy.rb +17 -0
- data/spec/shared/binding_key_strategy.rb +35 -0
- data/spec/shared/conditional_get.rb +48 -0
- data/spec/shared/context_key_strategy.rb +24 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/spec_support.rb +5 -0
- data/spec/support/active_record.rb +36 -0
- data/spec/support/cache.rb +15 -0
- data/spec/support/garner.rb +5 -0
- data/spec/support/mongoid.rb +71 -0
- metadata +155 -157
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Binding::Key::CacheKey do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@mock = double('model')
|
7
|
+
@mock.stub(:cache_key) { 'mocks/4' }
|
8
|
+
end
|
9
|
+
|
10
|
+
subject { Garner::Strategies::Binding::Key::CacheKey }
|
11
|
+
|
12
|
+
it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
|
13
|
+
let(:known_bindings) { [@mock] }
|
14
|
+
let(:unknown_bindings) { [@mock.class] }
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'apply' do
|
18
|
+
it "returns the object's cache key, or nil" do
|
19
|
+
subject.apply(@mock).should eq 'mocks/4'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'with real objects' do
|
24
|
+
it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
|
25
|
+
let(:known_bindings) { [Activist.create, Monger.create] }
|
26
|
+
let(:unknown_bindings) { [Monger.identify('m1'), Monger] }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Binding::Key::SafeCacheKey do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@new_mock = double('model')
|
7
|
+
@new_mock.stub(:cache_key) { 'mocks/4' }
|
8
|
+
@persisted_mock = double('model')
|
9
|
+
@time_dot_now = Time.now
|
10
|
+
@persisted_mock.stub(:cache_key) { "mocks/4-#{@time_dot_now.utc.to_s(:number)}" }
|
11
|
+
@persisted_mock.stub(:updated_at) { @time_dot_now }
|
12
|
+
end
|
13
|
+
|
14
|
+
subject { Garner::Strategies::Binding::Key::SafeCacheKey }
|
15
|
+
|
16
|
+
it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
|
17
|
+
let(:known_bindings) { [@persisted_mock] }
|
18
|
+
let(:unknown_bindings) { [@new_mock] }
|
19
|
+
end
|
20
|
+
|
21
|
+
describe 'apply' do
|
22
|
+
it "returns the object's cache key + milliseconds if defined" do
|
23
|
+
timestamp = @time_dot_now.utc.to_s(:number)
|
24
|
+
subject.apply(@persisted_mock).should =~ /^mocks\/4-#{timestamp}.[0-9]{10}$/
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns nil if :cache_key is undefined or nil' do
|
28
|
+
@persisted_mock.unstub(:cache_key)
|
29
|
+
subject.apply(@persisted_mock).should be_nil
|
30
|
+
@persisted_mock.stub(:cache_key) { nil }
|
31
|
+
subject.apply(@persisted_mock).should be_nil
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'returns nil if :updated_at is undefined or nil' do
|
35
|
+
@persisted_mock.unstub(:updated_at)
|
36
|
+
subject.apply(@persisted_mock).should be_nil
|
37
|
+
@persisted_mock.stub(:updated_at) { nil }
|
38
|
+
subject.apply(@persisted_mock).should be_nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context 'with real objects' do
|
43
|
+
before(:each) do
|
44
|
+
Garner.configure do |config|
|
45
|
+
config.mongoid_identity_fields = [:_id, :_slugs]
|
46
|
+
end
|
47
|
+
|
48
|
+
@monger = Monger.create(name: 'M1')
|
49
|
+
@food = Food.create(name: 'F1')
|
50
|
+
end
|
51
|
+
|
52
|
+
it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
|
53
|
+
let(:known_bindings) do
|
54
|
+
[Activist.create, @monger, Monger.identify(@monger.id), Monger.identify('m1'), Monger]
|
55
|
+
end
|
56
|
+
let(:unknown_bindings) do
|
57
|
+
[Monger.identify('m2'), Food.identify(nil), Monger.new, Activist.new]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Context::Key::Caller do
|
4
|
+
before(:each) do
|
5
|
+
@cache_identity = Garner::Cache::Identity.new
|
6
|
+
@mock_context = double('object')
|
7
|
+
end
|
8
|
+
|
9
|
+
subject { Garner::Strategies::Context::Key::Caller }
|
10
|
+
|
11
|
+
it_behaves_like 'Garner::Strategies::Context::Key strategy'
|
12
|
+
|
13
|
+
it 'ignores nil caller' do
|
14
|
+
@mock_context.stub(:caller) { nil }
|
15
|
+
subject.apply(@cache_identity, @mock_context)
|
16
|
+
@cache_identity.key_hash[:caller].should be_nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'ignores nil caller location' do
|
20
|
+
@mock_context.stub(:caller) { [nil] }
|
21
|
+
subject.apply(@cache_identity, @mock_context)
|
22
|
+
@cache_identity.key_hash[:caller].should be_nil
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'ignores blank caller location' do
|
26
|
+
@mock_context.stub(:caller) { [''] }
|
27
|
+
subject.apply(@cache_identity, @mock_context)
|
28
|
+
@cache_identity.key_hash[:caller].should be_nil
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'with default Garner.config.caller_root' do
|
32
|
+
before(:each) do
|
33
|
+
raw_gemfile_parent = File.join(__FILE__, '..', '..', '..', '..', '..', '..')
|
34
|
+
@gemfile_root = Pathname.new(raw_gemfile_parent).realpath.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'sets default_root to the nearest ancestor with a Gemfile' do
|
38
|
+
subject.default_root.should eq @gemfile_root
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sets Garner.config.caller_root to the nearest ancestor with a Gemfile' do
|
42
|
+
Garner.config.caller_root.should eq @gemfile_root
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'sets an appropriate value for :caller' do
|
46
|
+
truncated = __FILE__.gsub(@gemfile_root + File::SEPARATOR, '')
|
47
|
+
subject.apply(@cache_identity, self)
|
48
|
+
@cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'with Rails.root defined' do
|
53
|
+
before(:each) do
|
54
|
+
class Rails
|
55
|
+
end
|
56
|
+
Rails.stub(:root) { Pathname.new(File.dirname(__FILE__)) }
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'sets default_root to Rails.root' do
|
60
|
+
subject.default_root.should eq ::Rails.root.realpath.to_s
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'sets Garner.config.caller_root to Rails.root' do
|
64
|
+
Garner.config.caller_root.should eq ::Rails.root.realpath.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'sets an appropriate value for :caller' do
|
68
|
+
truncated = File.basename(__FILE__)
|
69
|
+
subject.apply(@cache_identity, self)
|
70
|
+
@cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'with custom Garner.config.caller_root' do
|
75
|
+
before(:each) do
|
76
|
+
Garner.configure do |config|
|
77
|
+
config.caller_root = File.dirname(__FILE__)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'sets an appropriate value for :caller' do
|
82
|
+
truncated = File.basename(__FILE__)
|
83
|
+
subject.apply(@cache_identity, self)
|
84
|
+
@cache_identity.key_hash[:caller].should eq "#{truncated}:#{__LINE__ - 1}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'with Garner.config.caller_root unset' do
|
89
|
+
before(:each) do
|
90
|
+
Garner.configure do |config|
|
91
|
+
config.caller_root = nil
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'sets an appropriate value for :caller' do
|
96
|
+
subject.apply(@cache_identity, self)
|
97
|
+
@cache_identity.key_hash[:caller].should eq "#{__FILE__}:#{__LINE__ - 1}"
|
98
|
+
end
|
99
|
+
|
100
|
+
it "doesn't require ActiveSupport" do
|
101
|
+
String.any_instance.stub(:blank?) { fail NoMethodError.new }
|
102
|
+
subject.apply(@cache_identity, self)
|
103
|
+
@cache_identity.key_hash[:caller].should eq "#{__FILE__}:#{__LINE__ - 1}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Context::Key::Jsonp do
|
4
|
+
before(:each) do
|
5
|
+
@cache_identity = Garner::Cache::Identity.new
|
6
|
+
@request = Rack::Request.new('REQUEST_METHOD' => 'GET', 'QUERY_STRING' => 'callback=jQuery21435&_=34234')
|
7
|
+
|
8
|
+
@mock_context = double('object')
|
9
|
+
@mock_context.stub(:request) { @request }
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { Garner::Strategies::Context::Key::Jsonp }
|
13
|
+
|
14
|
+
it_behaves_like 'Garner::Strategies::Context::Key strategy'
|
15
|
+
|
16
|
+
it 'removes JSONP params from the key hash' do
|
17
|
+
request_get = Garner::Strategies::Context::Key::RequestGet
|
18
|
+
applied_identity = request_get.apply(@cache_identity, @mock_context)
|
19
|
+
subject.apply(applied_identity, @mock_context)
|
20
|
+
@cache_identity.key_hash[:request_params].should eq({})
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Context::Key::RequestGet do
|
4
|
+
%w(GET HEAD).each do |method|
|
5
|
+
context method do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@cache_identity = Garner::Cache::Identity.new
|
9
|
+
@request = Rack::Request.new('REQUEST_METHOD' => method, 'QUERY_STRING' => 'foo=bar')
|
10
|
+
|
11
|
+
@mock_context = double('object')
|
12
|
+
@mock_context.stub(:request) { @request }
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { Garner::Strategies::Context::Key::RequestGet }
|
16
|
+
|
17
|
+
it_behaves_like 'Garner::Strategies::Context::Key strategy'
|
18
|
+
|
19
|
+
it 'adds :request_params to the key' do
|
20
|
+
subject.apply(@cache_identity, @mock_context)
|
21
|
+
@cache_identity.key_hash[:request_params].should eq('foo' => 'bar')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'appends to an existing key hash' do
|
25
|
+
@cache_identity.key(x: :y)
|
26
|
+
subject.apply(@cache_identity, @mock_context).key_hash.should eq(
|
27
|
+
x: :y,
|
28
|
+
request_params: { 'foo' => 'bar' }
|
29
|
+
)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Context::Key::RequestPath do
|
4
|
+
before(:each) do
|
5
|
+
@cache_identity = Garner::Cache::Identity.new
|
6
|
+
@request = Rack::Request.new('PATH_INFO' => '/foo')
|
7
|
+
|
8
|
+
@mock_context = double('object')
|
9
|
+
@mock_context.stub(:request) { @request }
|
10
|
+
end
|
11
|
+
|
12
|
+
subject { Garner::Strategies::Context::Key::RequestPath }
|
13
|
+
|
14
|
+
it_behaves_like 'Garner::Strategies::Context::Key strategy'
|
15
|
+
|
16
|
+
it 'adds :request_params to the key' do
|
17
|
+
subject.apply(@cache_identity, @mock_context)
|
18
|
+
@cache_identity.key_hash[:request_path].should eq '/foo'
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'appends to an existing key hash' do
|
22
|
+
@cache_identity.key(x: :y)
|
23
|
+
subject.apply(@cache_identity, @mock_context).key_hash.should eq(
|
24
|
+
x: :y,
|
25
|
+
request_path: '/foo'
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Garner::Strategies::Context::Key::RequestPost do
|
4
|
+
before(:each) do
|
5
|
+
@cache_identity = Garner::Cache::Identity.new
|
6
|
+
@request = Rack::Request.new(
|
7
|
+
Rack::MockRequest.env_for(
|
8
|
+
'/?foo=quux',
|
9
|
+
'REQUEST_METHOD' => 'POST',
|
10
|
+
:input => 'foo=bar'
|
11
|
+
)
|
12
|
+
)
|
13
|
+
|
14
|
+
@mock_context = double('object')
|
15
|
+
@mock_context.stub(:request) { @request }
|
16
|
+
end
|
17
|
+
|
18
|
+
subject { Garner::Strategies::Context::Key::RequestPost }
|
19
|
+
|
20
|
+
it_behaves_like 'Garner::Strategies::Context::Key strategy'
|
21
|
+
|
22
|
+
it 'adds :request_params to the key' do
|
23
|
+
subject.apply(@cache_identity, @mock_context)
|
24
|
+
@cache_identity.key_hash[:request_params].should eq('foo' => 'bar')
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'appends to an existing key hash' do
|
28
|
+
@cache_identity.key(x: :y)
|
29
|
+
subject.apply(@cache_identity, @mock_context).key_hash.should eq(
|
30
|
+
x: :y,
|
31
|
+
request_params: { 'foo' => 'bar' }
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'ActiveRecord integration' do
|
4
|
+
context 'using the Garner::Strategies::Binding::Key::CacheKey strategy' do
|
5
|
+
|
6
|
+
describe 'cache key generation' do
|
7
|
+
subject { Garner::Strategies::Binding::Key::CacheKey }
|
8
|
+
|
9
|
+
it_behaves_like 'Garner::Strategies::Binding::Key strategy' do
|
10
|
+
let(:known_bindings) { [Activist.create, Activist.new] }
|
11
|
+
let(:unknown_bindings) { [Activist] }
|
12
|
+
end
|
13
|
+
|
14
|
+
it "returns the object's cache key, or nil" do
|
15
|
+
new_activist = Activist.new
|
16
|
+
subject.apply(new_activist).should eq 'activists/new'
|
17
|
+
|
18
|
+
persisted_activist = Activist.create
|
19
|
+
timestamp = persisted_activist.updated_at.utc.to_s(persisted_activist.cache_timestamp_format)
|
20
|
+
expected_key = "activists/#{persisted_activist.id}-#{timestamp}"
|
21
|
+
subject.apply(persisted_activist).should eq expected_key
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe 'garner_cache_key' do
|
26
|
+
context 'instance' do
|
27
|
+
subject { Activist.create }
|
28
|
+
|
29
|
+
it 'returns a non-nil cache_key' do
|
30
|
+
subject.garner_cache_key.should_not be_nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'class' do
|
35
|
+
subject { Activist }
|
36
|
+
|
37
|
+
it 'should not ' do
|
38
|
+
expect { subject.garner_cache_key }.not_to raise_error
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'garner/mixins/rack'
|
3
|
+
require 'grape'
|
4
|
+
|
5
|
+
describe 'Grape integration' do
|
6
|
+
class TestCachebuster < Grape::Middleware::Base
|
7
|
+
def after
|
8
|
+
@app_response[1]['Expires'] = Time.at(0).utc.to_s
|
9
|
+
@app_response
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:app) do
|
14
|
+
class TestGrapeApp < Grape::API
|
15
|
+
helpers Garner::Mixins::Rack
|
16
|
+
use Rack::ConditionalGet
|
17
|
+
use Rack::ETag
|
18
|
+
use TestCachebuster
|
19
|
+
|
20
|
+
format :json
|
21
|
+
|
22
|
+
get '/' do
|
23
|
+
garner do
|
24
|
+
{ meaning_of_life: 42 }.to_json
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
TestGrapeApp.new
|
30
|
+
end
|
31
|
+
|
32
|
+
it_behaves_like 'Rack::ConditionalGet server'
|
33
|
+
end
|
@@ -0,0 +1,355 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'garner/mixins/mongoid'
|
3
|
+
|
4
|
+
describe 'Mongoid integration' do
|
5
|
+
before(:each) do
|
6
|
+
@app = Class.new.tap do |app|
|
7
|
+
app.send(:extend, Garner::Cache::Context)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
{
|
12
|
+
# Garner::Strategies::Binding::Key::SafeCacheKey => Garner::Strategies::Binding::Invalidation::Touch,
|
13
|
+
Garner::Strategies::Binding::Key::BindingIndex => Garner::Strategies::Binding::Invalidation::BindingIndex
|
14
|
+
}.each do |key_strategy, invalidation_strategy|
|
15
|
+
context "using #{key_strategy} with #{invalidation_strategy}" do
|
16
|
+
before(:each) do
|
17
|
+
Garner.configure do |config|
|
18
|
+
config.binding_key_strategy = key_strategy
|
19
|
+
config.binding_invalidation_strategy = invalidation_strategy
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'end-to-end caching and invalidation' do
|
24
|
+
context 'binding at the instance level' do
|
25
|
+
before(:each) do
|
26
|
+
@object = Monger.create!(name: 'M1')
|
27
|
+
end
|
28
|
+
|
29
|
+
describe 'garnered_find' do
|
30
|
+
before(:each) do
|
31
|
+
Garner.configure do |config|
|
32
|
+
config.mongoid_identity_fields = [:_id, :_slugs]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'caches one copy across all callers' do
|
37
|
+
Monger.stub(:find) { @object }
|
38
|
+
Monger.should_receive(:find).once
|
39
|
+
2.times { Monger.garnered_find('m1') }
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'returns the instance requested' do
|
43
|
+
Monger.garnered_find('m1').should eq @object
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'can be called with an array of one object, and will return an array' do
|
47
|
+
Monger.garnered_find(['m1']).should eq [@object]
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'is invalidated on changing identity field' do
|
51
|
+
Monger.garnered_find('m1').name.should eq 'M1'
|
52
|
+
@object.update_attributes!(name: 'M2')
|
53
|
+
Monger.garnered_find('m1').name.should eq 'M2'
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'is invalidated on destruction' do
|
57
|
+
Monger.garnered_find('m1').name.should eq 'M1'
|
58
|
+
@object.destroy
|
59
|
+
Monger.garnered_find('m1').should be_nil
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'with case-insensitive find' do
|
63
|
+
before(:each) do
|
64
|
+
find_method = Monger.method(:find)
|
65
|
+
Monger.stub(:find) do |param|
|
66
|
+
find_method.call(param.to_s.downcase)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'does not cache a nil identity' do
|
71
|
+
Monger.garnered_find('M1').should eq @object
|
72
|
+
Monger.garnered_find('foobar').should be_nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'on finding multiple objects' do
|
77
|
+
before(:each) do
|
78
|
+
@object2 = Monger.create!(name: 'M2')
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'returns the instances requested' do
|
82
|
+
Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
|
83
|
+
end
|
84
|
+
|
85
|
+
it 'can take an array' do
|
86
|
+
Monger.garnered_find(%w(m1 m2)).should eq [@object, @object2]
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'is invalidated when one of the objects is changed' do
|
90
|
+
Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
|
91
|
+
@object2.update_attributes!(name: 'M3')
|
92
|
+
Monger.garnered_find('m1', 'm2').last.name.should eq 'M3'
|
93
|
+
end
|
94
|
+
|
95
|
+
it 'is invalidated when one of the objects is changed, passed as an array' do
|
96
|
+
Monger.garnered_find(%w(m1 m2)).should eq [@object, @object2]
|
97
|
+
@object2.update_attributes!(name: 'M3')
|
98
|
+
Monger.garnered_find(%w(m1 m2)).last.name.should eq 'M3'
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'does not return a match when the objects cannot be found' do
|
102
|
+
Monger.garnered_find('m3').should be_nil
|
103
|
+
Monger.garnered_find('m3', 'm4').should eq []
|
104
|
+
Monger.garnered_find(%w(m3 m4)).should eq []
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'does not return a match when some of the objects cannot be found, and returns those that can' do
|
108
|
+
Monger.garnered_find('m1', 'm2').should eq [@object, @object2]
|
109
|
+
@object2.destroy
|
110
|
+
Monger.garnered_find('m1', 'm2').should eq [@object]
|
111
|
+
end
|
112
|
+
|
113
|
+
it 'correctly returns a single object when first asked for a single object as an array' do
|
114
|
+
Monger.garnered_find(['m1']).should eq [@object]
|
115
|
+
Monger.garnered_find('m1').should eq @object
|
116
|
+
end
|
117
|
+
|
118
|
+
it 'correctly returns an array of a single object, when first asked for a single object' do
|
119
|
+
Monger.garnered_find('m1').should eq @object
|
120
|
+
Monger.garnered_find(['m1']).should eq [@object]
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'caches properly when called with an array' do
|
124
|
+
Monger.stub(:find) { @object }
|
125
|
+
Monger.should_receive(:find).once
|
126
|
+
2.times { Monger.garnered_find(%w(m1 m2)) }
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
[:find, :identify].each do |selection_method|
|
133
|
+
context "binding via #{selection_method}" do
|
134
|
+
let(:cached_object_namer) do
|
135
|
+
lambda do
|
136
|
+
binding = Monger.send(selection_method, @object.id)
|
137
|
+
object = Monger.find(@object.id)
|
138
|
+
@app.garner.bind(binding) { object.name }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
it 'invalidates on update' do
|
143
|
+
cached_object_namer.call.should eq 'M1'
|
144
|
+
@object.update_attributes!(name: 'M2')
|
145
|
+
cached_object_namer.call.should eq 'M2'
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'invalidates on destroy' do
|
149
|
+
cached_object_namer.call.should eq 'M1'
|
150
|
+
@object.destroy
|
151
|
+
cached_object_namer.should raise_error
|
152
|
+
end
|
153
|
+
|
154
|
+
it 'invalidates by explicit call to invalidate_garner_caches' do
|
155
|
+
cached_object_namer.call.should eq 'M1'
|
156
|
+
if Mongoid.mongoid3?
|
157
|
+
@object.set(:name, 'M2')
|
158
|
+
else
|
159
|
+
@object.set(name: 'M2')
|
160
|
+
end
|
161
|
+
@object.invalidate_garner_caches
|
162
|
+
cached_object_namer.call.should eq 'M2'
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'does not invalidate results for other like-classed objects' do
|
166
|
+
cached_object_namer.call.should eq 'M1'
|
167
|
+
if Mongoid.mongoid3?
|
168
|
+
@object.set(:name, 'M2')
|
169
|
+
else
|
170
|
+
@object.set(name: 'M2')
|
171
|
+
end
|
172
|
+
new_monger = Monger.create!(name: 'M3')
|
173
|
+
new_monger.update_attributes!(name: 'M4')
|
174
|
+
new_monger.destroy
|
175
|
+
|
176
|
+
cached_object_namer.call.should eq 'M1'
|
177
|
+
end
|
178
|
+
|
179
|
+
context 'with racing destruction' do
|
180
|
+
before(:each) do
|
181
|
+
# Define two Mongoid objects for the race
|
182
|
+
@monger1, @monger2 = 2.times.map { Monger.find(@object.id) }
|
183
|
+
end
|
184
|
+
|
185
|
+
it 'invalidates caches properly (Type I)' do
|
186
|
+
cached_object_namer.call
|
187
|
+
@monger1.remove
|
188
|
+
if Mongoid.mongoid3?
|
189
|
+
@monger2.set(:name, 'M2')
|
190
|
+
else
|
191
|
+
@monger2.set(name: 'M2')
|
192
|
+
end
|
193
|
+
@monger1.destroy
|
194
|
+
@monger2.save
|
195
|
+
cached_object_namer.should raise_error
|
196
|
+
end
|
197
|
+
|
198
|
+
it 'invalidates caches properly (Type II)' do
|
199
|
+
cached_object_namer.call
|
200
|
+
if Mongoid.mongoid3?
|
201
|
+
@monger2.set(:name, 'M2')
|
202
|
+
else
|
203
|
+
@monger2.set(name: 'M2')
|
204
|
+
end
|
205
|
+
@monger1.remove
|
206
|
+
@monger2.save
|
207
|
+
@monger1.destroy
|
208
|
+
cached_object_namer.should raise_error
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'with inheritance' do
|
213
|
+
before(:each) do
|
214
|
+
@monger = Monger.create!(name: 'M1')
|
215
|
+
@object = Cheese.create!(name: 'Swiss', monger: @monger)
|
216
|
+
end
|
217
|
+
|
218
|
+
let(:cached_object_namer) do
|
219
|
+
lambda do
|
220
|
+
binding = Food.send(selection_method, @object.id)
|
221
|
+
object = Food.find(@object.id)
|
222
|
+
@app.garner.bind(binding) { object.name }
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
it 'binds to the correct object' do
|
227
|
+
cached_object_namer.call.should eq 'Swiss'
|
228
|
+
@object.update_attributes!(name: 'Havarti')
|
229
|
+
cached_object_namer.call.should eq 'Havarti'
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context 'with an embedded document' do
|
234
|
+
before(:each) do
|
235
|
+
@monger = Monger.create!(name: 'M1')
|
236
|
+
@fish = @monger.create_fish(name: 'Trout')
|
237
|
+
end
|
238
|
+
|
239
|
+
let(:cached_object_namer) do
|
240
|
+
lambda do
|
241
|
+
found = Monger.where('fish._id' => @fish.id).first.fish
|
242
|
+
@app.garner.bind(found) { found.name }
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
it 'binds to the correct object' do
|
247
|
+
cached_object_namer.call.should eq 'Trout'
|
248
|
+
@fish.update_attributes!(name: 'Sockeye')
|
249
|
+
cached_object_namer.call.should eq 'Sockeye'
|
250
|
+
end
|
251
|
+
|
252
|
+
context 'with :invalidate_mongoid_root = true' do
|
253
|
+
before(:each) do
|
254
|
+
Garner.configure do |config|
|
255
|
+
config.invalidate_mongoid_root = true
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
let(:root_cached_object_namer) do
|
260
|
+
lambda do
|
261
|
+
binding = Monger.send(selection_method, @monger.id)
|
262
|
+
@app.garner.bind(binding) { @monger.fish.name }
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'invalidates the root document' do
|
267
|
+
root_cached_object_namer.call.should eq 'Trout'
|
268
|
+
@fish.update_attributes!(name: 'Sockeye')
|
269
|
+
root_cached_object_namer.call.should eq 'Sockeye'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
context 'with multiple identities' do
|
275
|
+
before(:each) do
|
276
|
+
Garner.configure do |config|
|
277
|
+
config.mongoid_identity_fields = [:_id, :_slugs]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
let(:cached_object_namer_by_slug) do
|
282
|
+
lambda do |slug|
|
283
|
+
binding = Monger.send(selection_method, slug)
|
284
|
+
object = Monger.find(slug)
|
285
|
+
@app.garner.bind(binding) { object.name }
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'invalidates all identities' do
|
290
|
+
cached_object_namer.call.should eq 'M1'
|
291
|
+
cached_object_namer_by_slug.call('m1').should eq 'M1'
|
292
|
+
@object.update_attributes!(name: 'M2')
|
293
|
+
cached_object_namer.call.should eq 'M2'
|
294
|
+
cached_object_namer_by_slug.call('m1').should eq 'M2'
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
context 'binding at the class level' do
|
302
|
+
['top-level class', 'subclass'].each do |level|
|
303
|
+
context "to a #{level}" do
|
304
|
+
let(:klass) { level == 'subclass' ? Cheese : Food }
|
305
|
+
|
306
|
+
let(:cached_object_name_concatenator) do
|
307
|
+
lambda do
|
308
|
+
@app.garner.bind(klass) do
|
309
|
+
klass.all.order_by(_id: :asc).map(&:name).join(', ')
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'invalidates on create' do
|
315
|
+
Cheese.create(name: 'M1')
|
316
|
+
cached_object_name_concatenator.call.should eq 'M1'
|
317
|
+
Cheese.create(name: 'M3')
|
318
|
+
cached_object_name_concatenator.call.should eq 'M1, M3'
|
319
|
+
end
|
320
|
+
|
321
|
+
it 'invalidates on update' do
|
322
|
+
m1 = Cheese.create(name: 'M1')
|
323
|
+
Cheese.create(name: 'M3')
|
324
|
+
cached_object_name_concatenator.call.should eq 'M1, M3'
|
325
|
+
m1.update_attributes(name: 'M2')
|
326
|
+
cached_object_name_concatenator.call.should eq 'M2, M3'
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'invalidates on destroy' do
|
330
|
+
m1 = Cheese.create(name: 'M1')
|
331
|
+
Cheese.create(name: 'M3')
|
332
|
+
cached_object_name_concatenator.call.should eq 'M1, M3'
|
333
|
+
m1.destroy
|
334
|
+
cached_object_name_concatenator.call.should eq 'M3'
|
335
|
+
end
|
336
|
+
|
337
|
+
it 'invalidates by explicit call to invalidate_garner_caches' do
|
338
|
+
m1 = Cheese.create(name: 'M1')
|
339
|
+
Cheese.create(name: 'M3')
|
340
|
+
cached_object_name_concatenator.call.should eq 'M1, M3'
|
341
|
+
if Mongoid.mongoid3?
|
342
|
+
m1.set(:name, 'M2')
|
343
|
+
else
|
344
|
+
m1.set(name: 'M2')
|
345
|
+
end
|
346
|
+
klass.invalidate_garner_caches
|
347
|
+
cached_object_name_concatenator.call.should eq 'M2, M3'
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
end
|