garner 0.4.0 → 0.4.1

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