garner 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +35 -0
  5. data/.travis.yml +13 -0
  6. data/CHANGELOG.md +130 -0
  7. data/CONTRIBUTING.md +118 -0
  8. data/Gemfile +3 -0
  9. data/README.md +1 -0
  10. data/Rakefile +39 -0
  11. data/UPGRADING.md +118 -0
  12. data/garner.gemspec +44 -0
  13. data/lib/garner.rb +21 -21
  14. data/lib/garner/cache.rb +13 -6
  15. data/lib/garner/cache/binding.rb +6 -14
  16. data/lib/garner/cache/context.rb +11 -12
  17. data/lib/garner/cache/identity.rb +1 -1
  18. data/lib/garner/config.rb +12 -7
  19. data/lib/garner/mixins/active_record.rb +3 -3
  20. data/lib/garner/mixins/active_record/base.rb +2 -2
  21. data/lib/garner/mixins/mongoid.rb +4 -4
  22. data/lib/garner/mixins/mongoid/document.rb +8 -12
  23. data/lib/garner/mixins/mongoid/identity.rb +5 -6
  24. data/lib/garner/mixins/rack.rb +1 -2
  25. data/lib/garner/strategies/binding/invalidation/base.rb +2 -4
  26. data/lib/garner/strategies/binding/invalidation/binding_index.rb +1 -3
  27. data/lib/garner/strategies/binding/invalidation/touch.rb +0 -2
  28. data/lib/garner/strategies/binding/key/base.rb +1 -3
  29. data/lib/garner/strategies/binding/key/binding_index.rb +3 -4
  30. data/lib/garner/strategies/binding/key/cache_key.rb +0 -2
  31. data/lib/garner/strategies/binding/key/safe_cache_key.rb +2 -3
  32. data/lib/garner/strategies/context/key/base.rb +1 -3
  33. data/lib/garner/strategies/context/key/caller.rb +9 -12
  34. data/lib/garner/strategies/context/key/jsonp.rb +3 -6
  35. data/lib/garner/strategies/context/key/request_get.rb +2 -4
  36. data/lib/garner/strategies/context/key/request_path.rb +1 -3
  37. data/lib/garner/strategies/context/key/request_post.rb +2 -4
  38. data/lib/garner/version.rb +1 -1
  39. data/spec/garner/cache/context_spec.rb +38 -0
  40. data/spec/garner/cache/identity_spec.rb +68 -0
  41. data/spec/garner/cache_spec.rb +49 -0
  42. data/spec/garner/config_spec.rb +17 -0
  43. data/spec/garner/mixins/mongoid/document_spec.rb +80 -0
  44. data/spec/garner/mixins/mongoid/identity_spec.rb +140 -0
  45. data/spec/garner/mixins/rack_spec.rb +48 -0
  46. data/spec/garner/strategies/binding/invalidation/binding_index_spec.rb +14 -0
  47. data/spec/garner/strategies/binding/invalidation/touch_spec.rb +23 -0
  48. data/spec/garner/strategies/binding/key/binding_index_spec.rb +245 -0
  49. data/spec/garner/strategies/binding/key/cache_key_spec.rb +29 -0
  50. data/spec/garner/strategies/binding/key/safe_cache_key_spec.rb +61 -0
  51. data/spec/garner/strategies/context/key/caller_spec.rb +106 -0
  52. data/spec/garner/strategies/context/key/jsonp_spec.rb +22 -0
  53. data/spec/garner/strategies/context/key/request_get_spec.rb +33 -0
  54. data/spec/garner/strategies/context/key/request_path_spec.rb +28 -0
  55. data/spec/garner/strategies/context/key/request_post_spec.rb +34 -0
  56. data/spec/garner/version_spec.rb +11 -0
  57. data/spec/integration/active_record_spec.rb +43 -0
  58. data/spec/integration/grape_spec.rb +33 -0
  59. data/spec/integration/mongoid_spec.rb +355 -0
  60. data/spec/integration/rack_spec.rb +77 -0
  61. data/spec/integration/sinatra_spec.rb +29 -0
  62. data/spec/performance/strategy_benchmark.rb +59 -0
  63. data/spec/performance/support/benchmark_context.rb +31 -0
  64. data/spec/performance/support/benchmark_context_wrapper.rb +67 -0
  65. data/spec/shared/binding_invalidation_strategy.rb +17 -0
  66. data/spec/shared/binding_key_strategy.rb +35 -0
  67. data/spec/shared/conditional_get.rb +48 -0
  68. data/spec/shared/context_key_strategy.rb +24 -0
  69. data/spec/spec_helper.rb +24 -0
  70. data/spec/spec_support.rb +5 -0
  71. data/spec/support/active_record.rb +36 -0
  72. data/spec/support/cache.rb +15 -0
  73. data/spec/support/garner.rb +5 -0
  74. data/spec/support/mongoid.rb +71 -0
  75. metadata +155 -157
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+ require 'garner/mixins/rack'
3
+ require 'securerandom'
4
+
5
+ describe 'Rack integration' do
6
+ include Rack::Test::Methods
7
+
8
+ let(:app) do
9
+ class TestRackApp
10
+ include Garner::Mixins::Rack
11
+
12
+ attr_accessor :request
13
+
14
+ def call(env)
15
+ @request = Rack::Request.new(env)
16
+ random1 = garner { SecureRandom.hex }
17
+ random2 = garner { SecureRandom.hex }
18
+ [
19
+ 200,
20
+ { 'Content-Type' => 'application/json' },
21
+ [random1, random2].to_json
22
+ ]
23
+ end
24
+ end
25
+
26
+ TestRackApp.new
27
+ end
28
+
29
+ context 'with the RequestGet strategy disabled' do
30
+ before(:each) do
31
+ Garner.configure do |config|
32
+ config.rack_context_key_strategies -= [Garner::Strategies::Context::Key::RequestGet]
33
+ end
34
+ end
35
+ it 'co-caches requests to the same path with different query strings' do
36
+ get '/foo?q=1'
37
+ response1 = JSON.parse(last_response.body)[0]
38
+ get '/foo?q=2'
39
+ response2 = JSON.parse(last_response.body)[0]
40
+ response1.should eq response2
41
+ end
42
+ end
43
+
44
+ context 'with default configuration' do
45
+ it 'bypasses cache if cache_enabled? returns false' do
46
+ TestRackApp.any_instance.stub(:cache_enabled?) { false }
47
+ get '/'
48
+ response1 = JSON.parse(last_response.body)[0]
49
+ get '/'
50
+ response2 = JSON.parse(last_response.body)[0]
51
+ response1.should_not == response2
52
+ end
53
+
54
+ it 'caches different results for different paths' do
55
+ get '/foo'
56
+ response1 = JSON.parse(last_response.body)[0]
57
+ get '/bar'
58
+ response2 = JSON.parse(last_response.body)[0]
59
+ response1.should_not == response2
60
+ end
61
+
62
+ it 'caches different results for different query strings' do
63
+ get '/foo?q=1'
64
+ response1 = JSON.parse(last_response.body)[0]
65
+ get '/foo?q=2'
66
+ response2 = JSON.parse(last_response.body)[0]
67
+ response1.should_not == response2
68
+ end
69
+
70
+ it 'caches multiple blocks separately within an endpoint' do
71
+ get '/'
72
+ random1 = JSON.parse(last_response.body)[0]
73
+ random2 = JSON.parse(last_response.body)[1]
74
+ random1.should_not == random2
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,29 @@
1
+ require 'spec_helper'
2
+ require 'garner/mixins/rack'
3
+ require 'sinatra'
4
+
5
+ describe 'Sinatra integration' do
6
+
7
+ let(:app) do
8
+ class TestSinatraApp < Sinatra::Base
9
+ helpers Garner::Mixins::Rack
10
+ use Rack::ConditionalGet
11
+ use Rack::ETag
12
+
13
+ before do
14
+ headers 'Expires' => Time.at(0).utc.to_s
15
+ end
16
+
17
+ get '/' do
18
+ garner do
19
+ { meaning_of_life: 42 }.to_json
20
+ end
21
+ end
22
+ end
23
+
24
+ TestSinatraApp.new
25
+ end
26
+
27
+ it_behaves_like 'Rack::ConditionalGet server'
28
+
29
+ end
@@ -0,0 +1,59 @@
1
+ require 'spec_support'
2
+ require 'method_profiler'
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+ require 'support/benchmark_context'
6
+ require 'support/benchmark_context_wrapper'
7
+
8
+ class StrategyBenchmark
9
+ attr_accessor :n, :d, :r
10
+
11
+ def initialize(options = {})
12
+ @n = options[:n] || 1000 # Number of iterations
13
+ @d = options[:d] || 1024 # Document payload size
14
+ @r = options[:r] || 4 # Recursive database calls per garner block
15
+ end
16
+
17
+ def run!
18
+ profiler = MethodProfiler.observe(BenchmarkContext)
19
+ self.class.strategy_pairs.each do |key_strategy, invalidation_strategy|
20
+ Garner.configure do |config|
21
+ config.mongoid_identity_fields = [:_id, :_slugs]
22
+ config.binding_key_strategy = key_strategy
23
+ config.binding_invalidation_strategy = invalidation_strategy
24
+ end
25
+
26
+ proxy = BenchmarkContextWrapper.new(d: d, r: r)
27
+
28
+ # Workaround for MethodProfiler bug
29
+ profiler.instance_variable_set(:@data, Hash.new { |h, k| h[k] = [] })
30
+ n.times do
31
+ # Randomize order of calls
32
+ [
33
+ :warm_virtual_fetch,
34
+ :cold_virtual_fetch,
35
+ :warm_class_fetch,
36
+ :cold_class_fetch,
37
+ :force_invalidate,
38
+ :soft_invalidate
39
+ ].shuffle.each { |method| proxy.send(method) }
40
+ end
41
+
42
+ puts "Key: #{key_strategy.to_s.split('::')[-1]}"
43
+ puts "Invalidation: #{invalidation_strategy.to_s.split('::')[-1]}"
44
+ puts profiler.report.sort_by(:method).order(:ascending)
45
+ puts "\n\n"
46
+ end
47
+ end
48
+
49
+ def self.strategy_pairs
50
+ {
51
+ Garner::Strategies::Binding::Key::Base =>
52
+ Garner::Strategies::Binding::Invalidation::Base,
53
+ Garner::Strategies::Binding::Key::SafeCacheKey =>
54
+ Garner::Strategies::Binding::Invalidation::Touch,
55
+ Garner::Strategies::Binding::Key::BindingIndex =>
56
+ Garner::Strategies::Binding::Invalidation::BindingIndex
57
+ }
58
+ end
59
+ end
@@ -0,0 +1,31 @@
1
+ class BenchmarkContext
2
+ include Garner::Cache::Context
3
+
4
+ def virtual_fetch(klass, handle, r)
5
+ garner.bind(klass.identify(handle)).key(caller: nil) do
6
+ (r - 1).times { klass.find(handle).to_json }
7
+ klass.find(handle).to_json
8
+ end
9
+ end
10
+ alias_method :a_warm_virtual_fetch, :virtual_fetch
11
+ alias_method :b_cold_virtual_fetch, :virtual_fetch
12
+
13
+ def class_fetch(klass, handle, r)
14
+ garner.bind(klass).key(caller: nil) do
15
+ (r - 1).times { klass.find(handle).to_json }
16
+ klass.find(handle).to_json
17
+ end
18
+ end
19
+ alias_method :c_warm_class_fetch, :class_fetch
20
+ alias_method :d_cold_class_fetch, :class_fetch
21
+
22
+ def force_invalidate(binding)
23
+ binding.invalidate_garner_caches
24
+ end
25
+ alias_method :e_force_invalidate, :force_invalidate
26
+
27
+ def soft_invalidate(binding)
28
+ binding.send(:_garner_after_update)
29
+ end
30
+ alias_method :f_soft_invalidate, :soft_invalidate
31
+ end
@@ -0,0 +1,67 @@
1
+ require 'securerandom'
2
+
3
+ class BenchmarkContextWrapper
4
+ include Garner::Cache::Context
5
+ attr_accessor :binding, :context, :r
6
+
7
+ def initialize(options = {})
8
+ setup!(options)
9
+ end
10
+
11
+ def setup!(options)
12
+ Monger.destroy_all
13
+ Garner.config.cache.clear
14
+ @binding = Monger.create!(name: 'M1')
15
+ @binding.update_attributes!(
16
+ subdocument: SecureRandom.hex(options[:d])
17
+ ) if options[:d]
18
+ @r = options[:r]
19
+ @context = BenchmarkContext.new
20
+
21
+ # Prime MongoDB cache
22
+ Monger.find(@binding.slug)
23
+ end
24
+
25
+ def warm_virtual_fetch
26
+ warm_caches
27
+ context.a_warm_virtual_fetch(binding.class, binding.slug, r)
28
+ end
29
+
30
+ def cold_virtual_fetch
31
+ update_binding
32
+ context.b_cold_virtual_fetch(binding.class, binding.slug, r)
33
+ end
34
+
35
+ def warm_class_fetch
36
+ warm_caches
37
+ context.c_warm_class_fetch(binding.class, binding.slug, r)
38
+ end
39
+
40
+ def cold_class_fetch
41
+ update_binding
42
+ context.d_cold_class_fetch(binding.class, binding.slug, r)
43
+ end
44
+
45
+ def force_invalidate
46
+ context.e_force_invalidate(binding)
47
+ end
48
+
49
+ def soft_invalidate
50
+ context.f_soft_invalidate(binding)
51
+ end
52
+
53
+ private
54
+
55
+ def warm_caches
56
+ klass, handle = binding.class, binding.slug
57
+ json = binding.reload.to_json
58
+ garner.bind(klass.identify(handle)).key(caller: nil) { json }
59
+ garner.bind(klass).key(caller: nil) { json }
60
+ end
61
+
62
+ def update_binding
63
+ # Randomly shuffle name between M1, M2, M3
64
+ name = (%w(M1 M2 M3) - [binding.reload.name]).sample
65
+ binding.update_attributes!(name: name)
66
+ end
67
+ end
@@ -0,0 +1,17 @@
1
+ # Shared examples for binding invalidation strategies. A valid strategy must
2
+ # implement apply(binding) and force_apply(binding)
3
+ shared_examples_for 'Garner::Strategies::Binding::Invalidation strategy' do
4
+ it 'inherits from Garner::Strategies::Binding::Invalidation::Base' do
5
+ subject.new.should be_a(Garner::Strategies::Binding::Invalidation::Base)
6
+ end
7
+
8
+ describe 'apply' do
9
+ it 'requires an argument' do
10
+ expect { subject.apply }.to raise_error
11
+ end
12
+
13
+ it 'operates on any binding' do
14
+ expect { subject.apply(double('foo')) }.not_to raise_error
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,35 @@
1
+ # Shared examples for binding strategies. A valid binding strategy must implement
2
+ # apply(binding).
3
+ shared_examples_for 'Garner::Strategies::Binding::Key strategy' do
4
+ it 'requires an argument' do
5
+ expect { subject.apply }.to raise_error
6
+ end
7
+
8
+ it 'inherits from Garner::Strategies::Binding::Key::Base' do
9
+ subject.new.should be_a(Garner::Strategies::Binding::Key::Base)
10
+ end
11
+
12
+ describe 'given a known binding' do
13
+ it 'returns a valid cache key' do
14
+ known_bindings.each do |binding|
15
+ subject.apply(binding).should be_a(String)
16
+ end
17
+ end
18
+
19
+ it 'returns the same cache key for an unchanged object' do
20
+ known_bindings.each do |binding|
21
+ key1 = subject.apply(binding)
22
+ key2 = subject.apply(binding)
23
+ key1.should eq key2
24
+ end
25
+ end
26
+ end
27
+
28
+ describe 'given an unknown binding' do
29
+ it 'returns a nil cache key' do
30
+ unknown_bindings.each do |binding|
31
+ subject.apply(binding).should be_nil
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # Shared examples for a proper Conditional GET server.
2
+ # To test a new Rack framework, define an app returned by app, with a
3
+ # single endpoint, "/".
4
+ shared_examples_for 'Rack::ConditionalGet server' do
5
+ include Rack::Test::Methods
6
+
7
+ def etag_for(body_str)
8
+ Rack::ETag.new(nil).send(:digest_body, [body_str]).first
9
+ end
10
+
11
+ before(:each) do
12
+ Garner.config.cache.clear
13
+ end
14
+
15
+ it "writes the cached object's ETag from binding" do
16
+ get '/'
17
+ last_response.headers['ETag'].length.should eq 32 + 2
18
+ end
19
+
20
+ it 'sends a 304 response if content has not changed (If-None-Match)' do
21
+ get '/'
22
+ last_response.status.should eq 200
23
+ last_response.headers['ETag'].should eq %Q("#{etag_for(last_response.body)}")
24
+ get '/', {}, 'HTTP_IF_NONE_MATCH' => last_response.headers['ETag']
25
+ last_response.status.should eq 304
26
+ end
27
+
28
+ it 'sends a 200 response if content has changed (If-None-Match)' do
29
+ get '/'
30
+ last_response.status.should eq 200
31
+ get '/', {}, 'HTTP_IF_NONE_MATCH' => %Q("#{etag_for('foobar')}")
32
+ last_response.status.should eq 200
33
+ end
34
+
35
+ it 'sends a 200 response if content has changed (valid If-Modified-Since but invalid If-None-Match)' do
36
+ get '/'
37
+ last_response.status.should eq 200
38
+ get '/', {}, 'HTTP_IF_MODIFIED_SINCE' => (Time.now + 1).httpdate, 'HTTP_IF_NONE_MATCH' => %Q("#{etag_for(last_response.body)}")
39
+ last_response.status.should eq 200
40
+ end
41
+
42
+ it 'adds Cache-Control, Pragma and Expires headers' do
43
+ get '/'
44
+ last_response.headers['Cache-Control'].split(', ').sort.should eq %w(max-age=0 must-revalidate private)
45
+ last_response.headers['Pragma'].should be_nil
46
+ Time.parse(last_response.headers['Expires']).should be < Time.now
47
+ end
48
+ end
@@ -0,0 +1,24 @@
1
+ # Shared examples for key strategies. A valid key strategy must implement
2
+ # apply(identity, ruby_context = self).
3
+ shared_examples_for 'Garner::Strategies::Context::Key strategy' do
4
+ before(:each) do
5
+ @cache_identity = Garner::Cache::Identity.new
6
+ end
7
+
8
+ it 'inherits from Garner::Strategies::Context::Key::Base' do
9
+ subject.new.should be_a(Garner::Strategies::Context::Key::Base)
10
+ end
11
+
12
+ it 'requires a Garner::Cache::Identity' do
13
+ expect { subject.apply }.to raise_error
14
+ end
15
+
16
+ it 'does not require an explicit context, defaulting to self' do
17
+ expect { subject.apply(@cache_identity) }.to_not raise_error
18
+ end
19
+
20
+ it 'returns a Garner::Cache::Identity' do
21
+ modified_identity = subject.apply(@cache_identity, self)
22
+ modified_identity.should eq @cache_identity
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+
4
+ require 'coveralls'
5
+ Coveralls.wear!('test_frameworks') if ENV['CI']
6
+
7
+ require 'rspec'
8
+ require 'rack/test'
9
+
10
+ require 'garner'
11
+
12
+ # Load support files
13
+ require 'spec_support'
14
+
15
+ # Load shared examples
16
+ Dir["#{File.dirname(__FILE__)}/shared/*.rb"].each do |file|
17
+ require file
18
+ end
19
+
20
+ RSpec.configure do |rspec|
21
+ rspec.mock_with :rspec do |mocks|
22
+ mocks.patch_marshal_to_support_partial_doubles = true
23
+ end
24
+ end
@@ -0,0 +1,5 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+
3
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,36 @@
1
+ require 'active_record'
2
+ require 'garner/mixins/active_record'
3
+
4
+ # Set up in-memory SQLite connection for ActiveRecord
5
+ ActiveRecord::Base.establish_connection(
6
+ adapter: 'sqlite3',
7
+ database: ':memory:'
8
+ )
9
+
10
+ # Include mixin
11
+ module ActiveRecord
12
+ class Base
13
+ include Garner::Mixins::ActiveRecord::Base
14
+ end
15
+ end
16
+
17
+ # Stub classes
18
+ ActiveRecord::Migration.verbose = false
19
+ ActiveRecord::Migration.create_table :activists do |t|
20
+ t.string :name
21
+ t.timestamps
22
+ end
23
+
24
+ class Activist < ActiveRecord::Base
25
+ end
26
+
27
+ # Wrap each test example in a failing transaction to ensure a clean
28
+ # database for each run.
29
+ RSpec.configure do |config|
30
+ config.around do |example|
31
+ ActiveRecord::Base.transaction do
32
+ example.run
33
+ fail ActiveRecord::Rollback
34
+ end
35
+ end
36
+ end