garner 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -5,11 +5,10 @@ Garner is a cache layer for Ruby and Rack applications, supporting model and ins
5
5
 
6
6
  If you're not familiar with HTTP caching, ETags and If-Modified-Since, watch us introduce Garner in [From Zero to API Cache in 10 Minutes](http://www.confreaks.com/videos/986-goruco2012-from-zero-to-api-cache-w-grape-mongodb-in-10-minutes) at GoRuCo 2012.
7
7
 
8
- Stable Release
9
- --------------
8
+ Upgrading
9
+ ---------
10
10
 
11
- You're reading the documentation for the next release of Garner, which should be 0.4.0. See [UPGRADING](UPGRADING.md).
12
- The current stable release is [0.3.3](https://github.com/artsy/garner/blob/v0.3.3/README.md).
11
+ The current stable release of Garner is 0.4.1, and contains many breaking changes from the previous stable release, 0.3.3. For a summary of important changes, see [UPGRADING](UPGRADING.md).
13
12
 
14
13
  Usage
15
14
  -----
@@ -111,7 +110,17 @@ Order.find(3).invalidate_garner_caches
111
110
 
112
111
  ### ActiveRecord
113
112
 
114
- Garner provides rudimentary support for `ActiveRecord`. No mixins are required to bind to `ActiveRecord` objects. Just call `garner.bind(model)`, where `model` is an `ActiveRecord` object.
113
+ Garner provides rudimentary support for `ActiveRecord`. To use ActiveRecord models for Garner bindings, use `Garner::Mixins::ActiveRecord::Base`. You can set it up in an initializer:
114
+
115
+ ``` ruby
116
+ require "garner/mixins/active_record"
117
+
118
+ module ActiveRecord
119
+ class Base
120
+ include Garner::Mixins::ActiveRecord::Base
121
+ end
122
+ end
123
+ ```
115
124
 
116
125
 
117
126
  Under The Hood: Bindings
@@ -133,7 +142,7 @@ end
133
142
 
134
143
  Binding keys are computed via pluggable strategies, as are the rules for invalidating caches when a binding changes. By default, Garner uses `Garner::Strategies::Binding::Key::SafeCacheKey` to compute binding keys: this uses `cache_key` if defined on an object; otherwise it always bypasses cache. Similarly, Garner uses `Garner::Strategies::Binding::Invalidation::Touch` as its default invalidation strategy. This will call `:touch` on a document if it is defined; otherwise it will take no action.
135
144
 
136
- Additional binding and invalidation strategies can be written. To use them, set `Garner.config.binding_key_strategy` and `Garner.config.binding_invalidation_strategy`. Alternatively, for Mongoid-specific strategies, set `Garner.config.mongoid_binding_key_strategy` and `Garner.config.mongoid_binding_invalidation_strategy`.
145
+ Additional binding and invalidation strategies can be written. To use them, set `Garner.config.binding_key_strategy` and `Garner.config.binding_invalidation_strategy`.
137
146
 
138
147
 
139
148
  Under The Hood: Cache Context Keys
@@ -185,8 +194,6 @@ The full list of `Garner.config` attributes is:
185
194
  ```
186
195
  * `:binding_key_strategy`: Binding key strategy. Defaults to `Garner::Strategies::Binding::Key::SafeCacheKey`.
187
196
  * `:binding_invalidation_strategy`: Binding invalidation strategy. Defaults to `Garner::Strategies::Binding::Invalidation::Touch`.
188
- * `:mongoid_binding_key_strategy`: Mongoid-specific binding key strategy. Defaults to `Garner::Strategies::Binding::Key::SafeCacheKey`.
189
- * `:mongoid_binding_invalidation_strategy`: Mongoid-specific binding invalidation strategy. Defaults to `Garner::Strategies::Binding::Invalidation::Touch`.
190
197
  * `:mongoid_identity_fields`: Identity fields considered legal for the `identity` method. Defaults to `[:_id]`.
191
198
  * `:caller_root`: Root path of application, to be stripped out of value strings generated by the `Caller` context key strategy. Defaults to `Rails.root` if in a Rails environment; otherwise to the nearest ancestor directory containing a Gemfile.
192
199
 
data/lib/garner.rb CHANGED
@@ -17,10 +17,12 @@ require "garner/strategies/context/key/jsonp"
17
17
  require "garner/strategies/binding/key/base"
18
18
  require "garner/strategies/binding/key/cache_key"
19
19
  require "garner/strategies/binding/key/safe_cache_key"
20
+ require "garner/strategies/binding/key/binding_index"
20
21
 
21
22
  # Invalidation strategies
22
23
  require "garner/strategies/binding/invalidation/base"
23
24
  require "garner/strategies/binding/invalidation/touch"
25
+ require "garner/strategies/binding/invalidation/binding_index"
24
26
 
25
27
  # Cache
26
28
  require "garner/cache"
@@ -5,6 +5,9 @@ Garner.config.option(:binding_key_strategy, {
5
5
  Garner.config.option(:binding_invalidation_strategy, {
6
6
  :default => Garner::Strategies::Binding::Invalidation::Touch
7
7
  })
8
+ Garner.config.option(:mongoid_identity_fields, {
9
+ :default => [:_id]
10
+ })
8
11
 
9
12
  module Garner
10
13
  module Cache
@@ -18,6 +21,8 @@ module Garner
18
21
  end
19
22
 
20
23
  # Apply the cache key strategy to this binding.
24
+ #
25
+ # @return [String] A cache key string.
21
26
  def garner_cache_key
22
27
  key_strategy.apply(self)
23
28
  end
@@ -30,8 +35,11 @@ module Garner
30
35
  end
31
36
 
32
37
  # Apply the invalidation strategy to this binding.
38
+ #
39
+ # @return [Boolean] Returns true on success.
33
40
  def invalidate_garner_caches
34
41
  invalidation_strategy.apply(self)
42
+ true
35
43
  end
36
44
 
37
45
  protected
@@ -2,6 +2,14 @@
2
2
  Garner.config.option(:context_key_strategies, {
3
3
  :default => [Garner::Strategies::Context::Key::Caller]
4
4
  })
5
+ Garner.config.option(:rack_context_key_strategies, {
6
+ :default => [
7
+ Garner::Strategies::Context::Key::Caller,
8
+ Garner::Strategies::Context::Key::RequestGet,
9
+ Garner::Strategies::Context::Key::RequestPost,
10
+ Garner::Strategies::Context::Key::RequestPath
11
+ ]
12
+ })
5
13
 
6
14
  module Garner
7
15
  module Cache
@@ -0,0 +1,4 @@
1
+ require "garner"
2
+ require "active_record"
3
+
4
+ require "garner/mixins/active_record/base"
@@ -0,0 +1,17 @@
1
+ require "garner"
2
+ require "active_record"
3
+
4
+ module Garner
5
+ module Mixins
6
+ module ActiveRecord
7
+ module Base
8
+ extend ActiveSupport::Concern
9
+ include Garner::Cache::Binding
10
+
11
+ included do
12
+ extend Garner::Cache::Binding
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,4 +1,5 @@
1
1
  require "garner"
2
+ require "mongoid"
2
3
 
3
4
  require "garner/mixins/mongoid/document"
4
5
  require "garner/mixins/mongoid/identity"
@@ -1,12 +1,3 @@
1
- # Set up Garner configuration parameters
2
- Garner.config.option(:mongoid_binding_key_strategy, {
3
- :default => Garner.config.binding_key_strategy
4
- })
5
-
6
- Garner.config.option(:mongoid_binding_invalidation_strategy, {
7
- :default => Garner.config.binding_invalidation_strategy
8
- })
9
-
10
1
  module Garner
11
2
  module Mixins
12
3
  module Mongoid
@@ -14,45 +5,49 @@ module Garner
14
5
  extend ActiveSupport::Concern
15
6
  include Garner::Cache::Binding
16
7
 
17
- def key_strategy
18
- Garner.config.mongoid_binding_key_strategy
8
+ def proxied_classes
9
+ self.class.mongoid_superclasses
19
10
  end
20
11
 
21
- def invalidation_strategy
22
- Garner.config.mongoid_binding_invalidation_strategy
12
+ def identity_string
13
+ "#{self.class.name}/id=#{id}"
23
14
  end
24
15
 
25
16
  included do
26
17
  extend Garner::Cache::Binding
27
18
 
28
- def self.cache_key
29
- _latest_by_updated_at.try(:cache_key)
30
- end
31
-
32
- def self.touch
33
- _latest_by_updated_at.try(:touch)
34
- end
35
-
36
- def self.updated_at
37
- _latest_by_updated_at.try(:updated_at)
38
- end
39
-
40
- def self.key_strategy
41
- Garner.config.mongoid_binding_key_strategy
19
+ # Return an array of this class and all Mongoid superclasses.
20
+ #
21
+ # @return [Array] An array of classes.
22
+ def self.mongoid_superclasses
23
+ if superclass.include?(Mongoid::Document)
24
+ [self] + superclass.mongoid_superclasses
25
+ else
26
+ [self]
27
+ end
42
28
  end
43
29
 
44
- def self.invalidation_strategy
45
- Garner.config.mongoid_binding_invalidation_strategy
30
+ # Return an object that can act as a binding on this class's behalf.
31
+ #
32
+ # @return [Mongoid::Document]
33
+ def self.proxy_binding
34
+ _latest_by_updated_at
46
35
  end
47
36
 
48
- def self.identify(id)
49
- Garner::Mixins::Mongoid::Identity.from_class_and_id(self, id)
37
+ def self.identify(handle)
38
+ Mongoid::Identity.from_class_and_handle(self, handle)
50
39
  end
51
40
 
52
- def self.garnered_find(id)
53
- return nil unless (binding = identify(id))
41
+ # Find an object by _id, or other findable field, first trying to
42
+ # fetch from Garner's cache.
43
+ #
44
+ # @return [Mongoid::Document]
45
+ def self.garnered_find(handle)
46
+ return nil unless (binding = identify(handle))
54
47
  identity = Garner::Cache::Identity.new
55
- identity.bind(binding).key({ :source => :garnered_find }) { find(id) }
48
+ identity.bind(binding).key({ :source => :garnered_find }) do
49
+ find(handle)
50
+ end
56
51
  end
57
52
 
58
53
  after_create :_garner_after_create
@@ -1,22 +1,24 @@
1
- # Set up Garner configuration parameters
2
- Garner.config.option(:mongoid_identity_fields, {
3
- :default => [:_id]
4
- })
5
-
6
1
  module Garner
7
2
  module Mixins
8
3
  module Mongoid
9
4
  class Identity
10
5
  include Garner::Cache::Binding
11
6
 
12
- attr_accessor :document, :collection_name, :conditions
7
+ attr_accessor :klass, :handle, :proxy_binding, :conditions
13
8
 
14
- def self.from_class_and_id(klass, id)
9
+ # Instantiate a new Mongoid::Identity.
10
+ #
11
+ # @param klass [Class] A
12
+ # @param handle [Object] A String, Fixnum, BSON::ObjectId, etc.
13
+ # identifying the object.
14
+ # @return [Garner::Mixins::Mongoid::Identity]
15
+ def self.from_class_and_handle(klass, handle)
15
16
  validate_class!(klass)
16
17
 
17
18
  self.new.tap do |identity|
18
- identity.collection_name = klass.collection_name
19
- identity.conditions = conditions_for(klass, id)
19
+ identity.klass = klass
20
+ identity.handle = handle
21
+ identity.conditions = conditions_for(klass, handle)
20
22
  end
21
23
  end
22
24
 
@@ -24,57 +26,18 @@ module Garner
24
26
  @conditions = {}
25
27
  end
26
28
 
27
- def key_strategy
28
- Garner.config.mongoid_binding_key_strategy
29
- end
30
-
31
- def invalidation_strategy
32
- Garner.config.mongoid_binding_invalidation_strategy
33
- end
34
-
35
- def cache_key
36
- # See https://github.com/mongoid/mongoid/blob/f5ba1295/lib/mongoid/document.rb#L242
37
- if updated_at
38
- "#{model_cache_key}/#{_id}-#{updated_at.utc.to_s(:number)}"
39
- elsif _id
40
- "#{model_cache_key}/#{_id}"
41
- else
42
- "#{model_cache_key}/new"
43
- end
44
- end
45
-
46
- def model_cache_key
47
- if _type
48
- ActiveModel::Name.new(_type.constantize).cache_key
49
- else
50
- @collection_name.to_s
51
- end
52
- end
53
-
54
- def collection
55
- ::Mongoid.default_session[@collection_name]
56
- end
57
-
58
- def document
59
- return @document if @document
60
-
61
- collection.where(@conditions).select({
62
- :_id => 1,
63
- :_type => 1,
64
- :updated_at => 1
65
- }).limit(1).first
66
- end
67
-
68
- def _id
69
- document["_id"] if document
70
- end
71
-
72
- def updated_at
73
- document["updated_at"] if document
29
+ # Return an object that can act as a binding on this identity's behalf.
30
+ #
31
+ # @return [Mongoid::Document]
32
+ def proxy_binding
33
+ @proxy_binding ||= klass.where(conditions).only(:_id, :_type, :updated_at).first
74
34
  end
75
35
 
76
- def _type
77
- document["_type"] if document
36
+ # Stringize this identity for purposes of marshaling.
37
+ #
38
+ # @return [String]
39
+ def to_s
40
+ "#{self.class.name}/klass=#{klass},handle=#{handle}"
78
41
  end
79
42
 
80
43
  private
@@ -86,11 +49,11 @@ module Garner
86
49
  end
87
50
  end
88
51
 
89
- def self.conditions_for(klass, id)
52
+ def self.conditions_for(klass, handle)
90
53
  # multiple-ID conditions
91
54
  conditions = {
92
55
  "$or" => Garner.config.mongoid_identity_fields.map { |field|
93
- { field => id }
56
+ { field => handle }
94
57
  }
95
58
  }
96
59
 
@@ -1,15 +1,5 @@
1
1
  require "garner"
2
2
 
3
- # Set up Garner configuration parameters
4
- Garner.config.option(:rack_context_key_strategies, {
5
- :default => [
6
- Garner::Strategies::Context::Key::Caller,
7
- Garner::Strategies::Context::Key::RequestGet,
8
- Garner::Strategies::Context::Key::RequestPost,
9
- Garner::Strategies::Context::Key::RequestPath
10
- ]
11
- })
12
-
13
3
  module Garner
14
4
  module Mixins
15
5
  module Rack
@@ -0,0 +1,35 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Invalidation
5
+ class BindingIndex < Base
6
+
7
+ # Specifies whether invalidation should happen on callbacks.
8
+ #
9
+ # @param kind [Symbol] One of :create, :update, :destroy
10
+ def self.apply_on_callback?(kind = nil)
11
+ true
12
+ end
13
+
14
+ # Force-invalidate an object binding. Used when bindings are
15
+ # explicitly invalidated, via binding.invalidate_garner_caches.
16
+ #
17
+ # @param binding [Object] The binding whose caches are to be
18
+ # invalidated.
19
+ def self.apply(binding)
20
+ Key::BindingIndex.write_canonical_binding_for(binding)
21
+ Key::BindingIndex.write_cache_key_for(binding)
22
+
23
+ # Invalidate proxied classes
24
+ if binding.respond_to?(:proxied_classes)
25
+ binding.proxied_classes.each do |klass|
26
+ Key::BindingIndex.write_cache_key_for(klass)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -8,7 +8,10 @@ module Garner
8
8
  #
9
9
  # @param kind [Symbol] One of :create, :update, :destroy
10
10
  def self.apply_on_callback?(kind = nil)
11
- false
11
+ # Only apply on destruction, so that class bindings remain
12
+ # valid, if the destroyed binding was not also the previous
13
+ # proxy_binding.
14
+ !!(kind == :destroy)
12
15
  end
13
16
 
14
17
  # Force-invalidate an object binding. Used when bindings are
@@ -17,6 +20,17 @@ module Garner
17
20
  # @param binding [Object] The binding whose caches are to be
18
21
  # invalidated.
19
22
  def self.apply(binding)
23
+ if binding.respond_to?(:destroyed?) &&
24
+ binding.destroyed? &&
25
+ binding.class.respond_to?(:proxy_binding)
26
+ # Binding is destroyed, but we must ensure that its class's
27
+ # proxy_binding is touched, if necessary.
28
+ binding = binding.class.proxy_binding
29
+
30
+ elsif binding.respond_to?(:proxy_binding)
31
+ binding = binding.proxy_binding
32
+ end
33
+
20
34
  binding.touch if binding.respond_to?(:touch)
21
35
  end
22
36
  end
@@ -0,0 +1,109 @@
1
+ module Garner
2
+ module Strategies
3
+ module Binding
4
+ module Key
5
+ class BindingIndex < Base
6
+ RANDOM_KEY_LENGTH = 12 # In bytes.
7
+
8
+ # Compute a cache key as follows:
9
+ # 1. Determine whether the binding is canonical.
10
+ # 2. If canonical, fetch the cache key stored for binding.class,
11
+ # binding.id
12
+ # 3. If not canonical, determine the canonical ID from a proxy
13
+ # binding. Proxy bindings must implement `proxy_binding` and
14
+ # `handle`.
15
+ #
16
+ # @param binding [Object] The object from which to compute a key.
17
+ # @return [String] A cache key string.
18
+ def self.apply(binding)
19
+ fetch_cache_key_for(binding)
20
+ end
21
+
22
+ # Fetch cache key, from cache, for the given binding. Generate a
23
+ # random key, if not already extant.
24
+ #
25
+ # @param binding [Object] The object from which to compute a key.
26
+ # @return [String] A cache key string.
27
+ def self.fetch_cache_key_for(binding)
28
+ canonical_binding = fetch_canonical_binding_for(binding)
29
+ key = index_key_for(canonical_binding)
30
+ Garner.config.cache.fetch(key) { new_cache_key_for(canonical_binding) }
31
+ end
32
+
33
+ # Overwrite cache key for the given binding.
34
+ #
35
+ # @param binding [Object] The object from which to compute a key.
36
+ # @return [String] A cache key string.
37
+ def self.write_cache_key_for(binding)
38
+ canonical_binding = fetch_canonical_binding_for(binding)
39
+ key = index_key_for(canonical_binding)
40
+ value = new_cache_key_for(canonical_binding)
41
+ value.tap { |v| Garner.config.cache.write(key, v) }
42
+ end
43
+
44
+ # Fetch canonical binding for the given binding.
45
+ #
46
+ # @param binding [Object] The object from which to compute a key.
47
+ # @return [Object] A class, ID pair.
48
+ def self.fetch_canonical_binding_for(binding)
49
+ return binding if canonical?(binding)
50
+ key = index_key_for(binding)
51
+ Garner.config.cache.fetch(key) { canonical_binding_for(binding) }
52
+ end
53
+
54
+ # Overwrite canonical binding for the given binding.
55
+ #
56
+ # @param binding [Object] The object from which to compute a key.
57
+ # @return [Object] A class, ID pair.
58
+ def self.write_canonical_binding_for(binding)
59
+ return binding if canonical?(binding)
60
+ key = index_key_for(binding)
61
+ value = canonical_binding_for(binding)
62
+ value.tap { |v| Garner.config.cache.write(key, v) }
63
+ end
64
+
65
+ # Return canonical binding for the given binding.
66
+ #
67
+ # @param binding [Object] The (possibly) non-canonical binding.
68
+ # @return binding [Object] The canonical binding.
69
+ def self.canonical_binding_for(binding)
70
+ if canonical?(binding)
71
+ binding
72
+ elsif binding.respond_to?(:proxy_binding)
73
+ canonical_binding_for(binding.proxy_binding)
74
+ else
75
+ nil
76
+ end
77
+ end
78
+
79
+ # Determine whether the given binding is canonical.
80
+ #
81
+ # @return [Boolean]
82
+ def self.canonical?(binding)
83
+ # TODO: Implement real logic for determining canonicity.
84
+ binding.is_a?(Mongoid::Document) ||
85
+ (binding.is_a?(Class) && binding.include?(Mongoid::Document))
86
+ end
87
+
88
+ private
89
+ def self.index_key_for(binding)
90
+ if binding.respond_to?(:identity_string)
91
+ binding_key = binding.identity_string
92
+ else
93
+ binding_key = binding.to_s
94
+ end
95
+
96
+ {
97
+ :strategy => self,
98
+ :proxied_binding => binding_key
99
+ }
100
+ end
101
+
102
+ def self.new_cache_key_for(binding)
103
+ SecureRandom.hex(RANDOM_KEY_LENGTH)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
@@ -15,6 +15,8 @@ module Garner
15
15
  # @param binding [Object] The object from which to compute a key.
16
16
  # @return [String] A cache key string.
17
17
  def self.apply(binding)
18
+ binding = binding.proxy_binding if binding.respond_to?(:proxy_binding)
19
+
18
20
  return unless binding.respond_to?(:cache_key) && binding.cache_key
19
21
  return unless binding.respond_to?(:updated_at) && binding.updated_at
20
22
 
@@ -1,3 +1,3 @@
1
1
  module Garner
2
- VERSION = "0.4.0"
2
+ VERSION = "0.4.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: garner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2013-06-17 00:00:00.000000000 Z
13
+ date: 2013-06-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: rack
@@ -268,6 +268,22 @@ dependencies:
268
268
  - - ! '>='
269
269
  - !ruby/object:Gem::Version
270
270
  version: '0'
271
+ - !ruby/object:Gem::Dependency
272
+ name: method_profiler
273
+ requirement: !ruby/object:Gem::Requirement
274
+ none: false
275
+ requirements:
276
+ - - ! '>='
277
+ - !ruby/object:Gem::Version
278
+ version: '0'
279
+ type: :development
280
+ prerelease: false
281
+ version_requirements: !ruby/object:Gem::Requirement
282
+ none: false
283
+ requirements:
284
+ - - ! '>='
285
+ - !ruby/object:Gem::Version
286
+ version: '0'
271
287
  - !ruby/object:Gem::Dependency
272
288
  name: pry
273
289
  requirement: !ruby/object:Gem::Requirement
@@ -347,13 +363,17 @@ files:
347
363
  - lib/garner/cache/context.rb
348
364
  - lib/garner/cache/identity.rb
349
365
  - lib/garner/config.rb
366
+ - lib/garner/mixins/active_record.rb
367
+ - lib/garner/mixins/active_record/base.rb
350
368
  - lib/garner/mixins/mongoid.rb
351
369
  - lib/garner/mixins/mongoid/document.rb
352
370
  - lib/garner/mixins/mongoid/identity.rb
353
371
  - lib/garner/mixins/rack.rb
354
372
  - lib/garner/strategies/binding/invalidation/base.rb
373
+ - lib/garner/strategies/binding/invalidation/binding_index.rb
355
374
  - lib/garner/strategies/binding/invalidation/touch.rb
356
375
  - lib/garner/strategies/binding/key/base.rb
376
+ - lib/garner/strategies/binding/key/binding_index.rb
357
377
  - lib/garner/strategies/binding/key/cache_key.rb
358
378
  - lib/garner/strategies/binding/key/safe_cache_key.rb
359
379
  - lib/garner/strategies/context/key/base.rb
@@ -380,7 +400,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
380
400
  version: '0'
381
401
  segments:
382
402
  - 0
383
- hash: 1734950059228816164
403
+ hash: 1915990174138923704
384
404
  required_rubygems_version: !ruby/object:Gem::Requirement
385
405
  none: false
386
406
  requirements: