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.
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