garner 0.3.3 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/LICENSE.md +1 -1
  2. data/README.md +116 -135
  3. data/lib/garner/cache/binding.rb +58 -0
  4. data/lib/garner/cache/context.rb +27 -0
  5. data/lib/garner/cache/identity.rb +45 -0
  6. data/lib/garner/cache.rb +41 -0
  7. data/lib/garner/config.rb +46 -15
  8. data/lib/garner/mixins/mongoid/document.rb +75 -0
  9. data/lib/garner/mixins/mongoid/identity.rb +106 -0
  10. data/lib/garner/mixins/mongoid.rb +4 -0
  11. data/lib/garner/mixins/rack.rb +45 -0
  12. data/lib/garner/strategies/binding/invalidation/base.rb +26 -0
  13. data/lib/garner/strategies/binding/invalidation/touch.rb +27 -0
  14. data/lib/garner/strategies/binding/key/base.rb +19 -0
  15. data/lib/garner/strategies/binding/key/cache_key.rb +19 -0
  16. data/lib/garner/strategies/binding/key/safe_cache_key.rb +33 -0
  17. data/lib/garner/strategies/context/key/base.rb +21 -0
  18. data/lib/garner/strategies/context/key/caller.rb +83 -0
  19. data/lib/garner/strategies/context/key/jsonp.rb +30 -0
  20. data/lib/garner/strategies/context/key/request_get.rb +30 -0
  21. data/lib/garner/strategies/context/key/request_path.rb +28 -0
  22. data/lib/garner/strategies/context/key/request_post.rb +30 -0
  23. data/lib/garner/version.rb +1 -1
  24. data/lib/garner.rb +29 -26
  25. metadata +122 -22
  26. data/lib/garner/cache/object_identity.rb +0 -249
  27. data/lib/garner/middleware/base.rb +0 -47
  28. data/lib/garner/middleware/cache/bust.rb +0 -20
  29. data/lib/garner/mixins/grape_cache.rb +0 -111
  30. data/lib/garner/mixins/mongoid_document.rb +0 -58
  31. data/lib/garner/strategies/cache/expiration_strategy.rb +0 -16
  32. data/lib/garner/strategies/etags/grape_strategy.rb +0 -32
  33. data/lib/garner/strategies/etags/marshal_strategy.rb +0 -16
  34. data/lib/garner/strategies/keys/caller_strategy.rb +0 -38
  35. data/lib/garner/strategies/keys/jsonp_strategy.rb +0 -24
  36. data/lib/garner/strategies/keys/key_strategy.rb +0 -21
  37. data/lib/garner/strategies/keys/request_get_strategy.rb +0 -24
  38. data/lib/garner/strategies/keys/request_path_strategy.rb +0 -21
  39. data/lib/garner/strategies/keys/request_post_strategy.rb +0 -24
  40. data/lib/garner/strategies/keys/version_strategy.rb +0 -29
data/LICENSE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2012 Art.sy, Frank Macreery, Daniel Doubrovkine & Contributors
3
+ Copyright (c) 2012-2013 Artsy, Frank Macreery, Daniel Doubrovkine & contributors.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining
6
6
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,91 +1,58 @@
1
- Garner [![Build Status](https://secure.travis-ci.org/artsy/garner.png)](http://travis-ci.org/artsy/garner)
1
+ Garner [![Build Status](https://secure.travis-ci.org/artsy/garner.png)](http://travis-ci.org/artsy/garner) [![Dependency Status](https://gemnasium.com/artsy/garner.png)](https://gemnasium.com/artsy/garner) [![Coverage Status](https://coveralls.io/repos/artsy/garner/badge.png)](https://coveralls.io/r/artsy/garner)
2
2
  ======
3
3
 
4
- Garner is a practical Rack-based cache implementation for RESTful APIs with support for HTTP 304 Not Modified based on time and ETags, model and instance binding and hierarchical invalidation. Garner is currently targeted at [Grape](https://github.com/intridea/grape), other systems may need some work.
4
+ Garner is a cache layer for Ruby and Rack applications, supporting model and instance binding and hierarchical invalidation. To "garner" means to gather data from various sources and to make it readily available in one place, kind of like a cache!
5
5
 
6
- To "garner" means to gather data from various sources and to make it readily available in one place, kind-of like a cache!
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
- Usage
9
- -----
8
+ Stable Release
9
+ --------------
10
10
 
11
- Add Garner to Gemfile with `gem "garner"` and run `bundle install`. Include the Garner mixin into your API. Currently Grape is supported out of the box. It's also recommended to prevent clients from caching dynamic data by default using the `Garner::Middleware::Cache::Bust` middleware. See below for a detailed explanation.
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).
12
13
 
13
- Note that if you are using Grape, `gem "garner"` must be listed AFTER `gem "grape"` in the Gemfile (see [#6](https://github.com/artsy/garner/issues/6)).
14
+ Usage
15
+ -----
14
16
 
15
- ```ruby
16
- class API < Grape::API
17
- use Garner::Middleware::Cache::Bust
18
- helpers Garner::Mixins::Grape::Cache
19
- end
20
- ```
17
+ ### Application Logic Caching
21
18
 
22
- To cache a value, invoke `cache` from within your API. Without any parameters it generates a key based on the source code location, request parameters and path, and stores the value in the cache configured as `Garner.config.cache`. The cache is automatically `Rails.cache` when mounted in Rails and an instance of `ActiveSupport::Cache::MemoryStore` otherwise.
19
+ Add Garner to your Gemfile with `gem "garner"` and run `bundle install`. Next, include the appropriate mixin in your app:
23
20
 
24
- ``` ruby
25
- get "/" do
26
- cache do
27
- { counter: 42 }
28
- end
29
- end
30
- ```
21
+ * For plain-old Ruby apps, `include Garner::Cache::Context`.
22
+ * For Rack apps, `include Garner::Mixins::Rack`. (This provides saner defaults for injecting request parameters into the cache context key. More on cache context keys later.)
31
23
 
32
- To enable support for the date-based `If-Modified-Since` and the ETag-based `If-None-Match`, use `cache_or_304`. If the data hasn't changed, the API will return `304 Not Modified` without a cache miss. For example, if the inside of a cached block is a database query, it will not be executed the second time. This is possible because Garner stores an entry for every cache binding with the last-modified timestamp and ETag.
24
+ Now, to use Garner's cache, invoke `garner` with a logic block from within your application. The result of the block will be computed once, and then stored in the cache.
33
25
 
34
26
  ``` ruby
35
- get "/" do
36
- cache_or_304 do
37
- { counter: 42 }
27
+ get "/system/counts/all" do
28
+ # Compute once and cache for subsequent reads
29
+ garner do
30
+ {
31
+ "orders_count" => Order.count,
32
+ "users_count" => User.count
33
+ }
38
34
  end
39
35
  end
40
36
  ```
41
37
 
42
- The cached value can also be bound to other models. For example, if a user has an address that may or may not change when the user is saved, you will want the cached address to be invalidated every time the user record changes.
38
+ The cached value can be bound to a particular model instance. For example, if a user has an address that may or may not change when the user is saved, you will want the cached address to be invalidated every time the user record is modified.
43
39
 
44
40
  ``` ruby
45
41
  get "/me/address" do
46
- cache_or_304({ bind: [ User, current_user.id ] }) do
42
+ # Invalidate when current_user is modified
43
+ garner.bind(current_user) do
47
44
  current_user.address
48
45
  end
49
46
  end
50
47
  ```
51
48
 
52
- ETag Generation Strategies
53
- --------------------------
54
-
55
- The primary purpose of the ETag header is to define a short string representation of a cached object that is both (a) deterministic and (b) unique, so that Garner's `cache_or_304` method can quickly determine whether a client's cached content matches the latest server object. As such, an MD5 hash applied to *any* object serialization would suffice. However, some applications may wish to control the manner in which ETags are generated, and so Garner supports arbitrary ETag strategies.
56
49
 
57
- The default strategy, `Garner::Strategies::ETags::Grape`, follows the serialization strategy used by Grape for coercing objects into JSON. Using this strategy, Garner will generate an ETag for each cache object that is identical to what `Rack::ETag` would return if that object was returned by Grape. This property could be useful for Grape applications.
50
+ ORM Integrations
51
+ ----------------
58
52
 
59
- Another, simpler strategy, `Garner::Strategies::ETags::Marshal`, simply applies an MD5 hash to `Marshal.dump(object)`. This strategy might be more applicable for applications not using Grape.
60
-
61
- An ETag strategy may be defined at application startup time:
62
-
63
- ```
64
- ETAG_STRATEGY = Garner::Strategies::ETags::Grape
65
- ```
53
+ ### Mongoid
66
54
 
67
-
68
- Binding Strategies
69
- ------------------
70
-
71
- The binding parameter can be an object, class, array of objects, or array of classes on which to bind the validity of the cached result contained in the subsequent block. If no bind argument is specified, the subsequent block result will remain valid until it expires due to natural causes (e.g., passage of default memcached expiry, or memcached overflow). Here are some examples of how to use the bind option.
72
-
73
- * `bind: { klass: Widget, object: { id: params[:id] } }` will cause the subsequent block result to be invalidated on any change to the `Widget` object whose `id` attribute equals `params[:id]`.
74
- * `bind: { klass: User, object: { id: current_user.id } }` will cause the subsequent block result to be invalidated on any change to the `User` object whose `id` attribute equals `current_user.id`. This is one way to bind a cache result to any change in the current user.
75
- * `bind: { klass: Widget }` will cause the subsequent block result to be invalidated on any change to any object of class `Widget`. This is the appropriate strategy for index paths like `/widgets`.
76
- * `bind: [{ klass: Widget }, { klass: User, object: { id: current_user.id } }]` will cause the subsequent block result to be invalidated on any change to either the current user, or any object of class `Widget`.
77
-
78
- Bind supports some nice shorthands.
79
-
80
- * `bind: [Widget]` is shorthand for `bind: { klass: Widget }`
81
- * `bind: [Widget, params[:id]]` is shorthand for `bind: { klass: Widget, object: { id: params[:slug] } }`
82
- * `bind: [User, { id: current_user.id }]` is shorthand for `bind: { klass: User, object: { id: current_user.id } }`
83
- * `bind: [[Widget], [User, { id: current_user.id }]]` is shorthand for `bind: [{ klass: Widget }, { klass: User, object: { id: current_user.id } }]`
84
-
85
- Invalidation
86
- ------------
87
-
88
- You must take care of data invalidation on save. Garner currently includes a mixin with support for [Mongoid](https://github.com/mongoid/mongoid). Extend `Mongoid::Document` as follows (eg. in `config/initializers/mongoid_document.rb`).
55
+ To use Mongoid documents and classes for Garner bindings, use `Garner::Mixins::Mongoid::Document`. You can set it up in an initializer:
89
56
 
90
57
  ``` ruby
91
58
  module Mongoid
@@ -95,96 +62,102 @@ module Mongoid
95
62
  end
96
63
  ```
97
64
 
98
- Please contribute other invalidation mixins.
65
+ This enables binding to Mongoid classes as well as instances. For example:
99
66
 
100
- Role-Based Caching
101
- ------------------
102
-
103
- Role-Based caching is a subset of the generic problem of binding data to groups of other objects. For example, a `Widget` may have a different representation for an `admin` vs. a `user`. In Garner you can inject something called a "key strategy" into the current key generation pipeline. A strategy is a plain module that must implement two methods: `apply` and `field`. The former applies a strategy to a key within a context and the latter is a unique name that is produced by the strategy.
67
+ ```ruby
68
+ get "/system/counts/orders" do
69
+ # Invalidate when any order is created, updated or deleted
70
+ garner.bind(Order) do
71
+ {
72
+ "orders_count" => Order.count,
73
+ }
74
+ end
75
+ end
76
+ ```
104
77
 
105
- The following example introduces the role of the current user into the cache key.
78
+ What if you want to bind a cache result to a persisted object that hasn't been retrieved yet? Consider the example of caching a particular order without a database query:
106
79
 
107
- ``` ruby
108
- module MyApp
109
- module Garner
110
- module RoleStrategy
111
- class << self
112
- def field
113
- :role
114
- end
115
- def apply(key, context = {})
116
- key = key ? key.dup : {}
117
- key[:role] = current_user.role
118
- key
119
- end
120
- end
121
- end
80
+ ```ruby
81
+ get "/order/:id" do
82
+ # Invalidate when Order.find(params[:id]) is modified
83
+ garner.bind(Order.identify(params[:id])) do
84
+ Order.find(params[:id])
122
85
  end
123
86
  end
124
87
  ```
125
88
 
126
- Garner key strategies are applied in order and can be currently set at application startup time.
89
+ In the above example, the `Order.identify` call will not result in a database query. Instead, it just communicates to Garner's cache sweeper that whenever the order with identity `params[:id]` is updated, this cache result should be invalidated. The `identify` method is provided by the Mongoid mixin. To use it, you should configure `Garner.config.mongoid_identity_fields`, e.g.:
127
90
 
91
+ ```ruby
92
+ Garner.configure.do |config|
93
+ config.mongoid_identity_fields = [:_id, :_slugs]
94
+ end
128
95
  ```
129
- Garner::Cache::ObjectIdentity::KEY_STRATEGIES = [
130
- Garner::Strategies::Keys::Caller, # support multiple calls from the same function
131
- MyApp::Garner::RoleStrategy, # custom strategy for role-based access
132
- Garner::Strategies::Keys::RequestPath # injects the HTTP request's URL
133
- ]
96
+
97
+ These may be scalar or array fields. Only uniquely-constrained fields should be used here; otherwise you risk caching the same result for two different blocks.
98
+
99
+ The Mongoid mixin also provides helper methods for cached `find` operations. The following code will fetch an order once (via `find`) from the database, and then fetch it from the cache on subsequent requests. The cache will be invalidated whenever the underlying `Order` changes in the database.
100
+
101
+ ```ruby
102
+ order = Order.garnered_find(3)
134
103
  ```
135
104
 
136
- This method of registration does need improvement, please contribute.
105
+ Explicit invalidation should be unnecessary, since callbacks are declared to invalidate the cache whenever a Mongoid object is created, updated or destroyed, but for special cases, `invalidate_garner_caches` may be called on a Mongoid object or class:
137
106
 
138
- Available Key Strategies
139
- ------------------------
107
+ ```ruby
108
+ Order.invalidate_garner_caches
109
+ Order.find(3).invalidate_garner_caches
110
+ ```
140
111
 
141
- * `Garner::Strategies::Keys::Caller` inserts the calling file and line number, allowing multiple calls from the same function to generate different keys. The caller can be specified explicitly by passing a value for `:caller` in the requesting context.
142
- * `Garner::Strategies::Keys::Version` inserts the output of a `version` method, when available, primarily targeted at API implementations.
143
- * `Garner::Strategies::Keys::Key` inserts the value of `:key` within the requested context, useful to explicitly declare an element of a cache key.
144
- * `Garner::Strategies::Keys::RequestGet` inserts the value of HTTP request's GET parameters into the cache key when `:request` is present in the context.
145
- * `Garner::Strategies::Keys::RequestPost` inserts the value of HTTP request's POST parameters into the cache key when `:request` is present in the context.
146
- * `Garner::Strategies::Keys::RequestPath` inserts the value of the HTTP request's path into the cache key when `:request` is present in the context.
112
+ ### ActiveRecord
147
113
 
148
- Fetching Objects Directly from Cache
149
- ------------------------------------
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.
150
115
 
151
- Garner supports fetching objects or collections of objects directly from cache by supplying a binding or an array of bindings.
152
116
 
153
- ``` ruby
154
- object_id = ...
155
- Garner::Cache::ObjectIdentity.cache({ bind: [ Model, { id: object_id }] }) do
156
- Model.find(object_id)
157
- end
158
- ```
117
+ Under The Hood: Bindings
118
+ ------------------------
159
119
 
160
- When fetching directly from the cache, it may be useful to supply a generational cache key in addition to the bindings. (When the generational component changes, the cache result is invalidated independent of the bindings' state.) E.g.:
120
+ As we've seen, a cache result can be bound to a model instance (e.g., `current_user`) or a virtual instance reference (`Order.identify(params[:id])`). In some cases, we may want to compose bindings:
161
121
 
162
122
  ```ruby
163
- Garner::Cache::ObjectIdentity.cache(bind: [Model, {id: object_id}], key: {v: '1'}) do
164
- # ...
123
+ get "/system/counts/all" do
124
+ # Invalidate when any order or user is modified
125
+ garner.bind(Order).bind(User) do
126
+ {
127
+ "orders_count" => Order.count,
128
+ "users_count" => User.count
129
+ }
130
+ end
165
131
  end
166
132
  ```
167
133
 
168
- Or, using the Grape mix-in:
134
+ 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
+
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`.
137
+
138
+
139
+ Under The Hood: Cache Context Keys
140
+ ----------------------------------
141
+
142
+ Explicit cache context keys are usually unnecessary in Garner. Given a cache binding, Garner will compute an appropriately unique cache key. Moreover, in the context of `Garner::Mixins::Rack`, Garner will compose the following key factors by default:
143
+
144
+ * `Garner::Strategies::Context::Key::Caller` inserts the calling file and line number, allowing multiple calls from the same function to generate different results.
145
+ * `Garner::Strategies::Context::Key::RequestGet` inserts the value of HTTP request's GET parameters into the cache key when `:request` is present in the context.
146
+ * `Garner::Strategies::Context::Key::RequestPost` inserts the value of HTTP request's POST parameters into the cache key when `:request` is present in the context.
147
+ * `Garner::Strategies::Context::Key::RequestPath` inserts the value of the HTTP request's path into the cache key when `:request` is present in the context.
148
+
149
+ Additional key factors may be specified explicitly using the `key` method. To see a specific example of this in action, let's consider the case of role-based caching. For example, an order may have a different representation for an admin versus an ordinary user:
169
150
 
170
151
  ```ruby
171
- cache_or_304(bind: [User, current_user.id], key: {v: '1'}) do
172
- # ...
152
+ get "/order/:id" do
153
+ garner.bind(Order.identify(params[:id])).key({ role: current_user.role }) do
154
+ Order.find(params[:id])
155
+ end
173
156
  end
174
157
  ```
175
158
 
176
- Various cache stores, including Memcached, support bulk read operations. The [Dalli gem](https://github.com/mperham/dalli) exposes this via the `read_multi` method. When invoked with a collection of bindings, Garner will call `read_multi` if available. This may significantly reduce the number of network roundtrips to the cache servers.
159
+ As with bindings, context key factors may be composed by calling `key()` multiple times on a `garner` invocation. The keys will be applied in the order in which they are called.
177
160
 
178
- ``` ruby
179
- object_ids = [ ... ]
180
- bindings = object_ids.map do |object_id|
181
- { bind: [ Model, { id: object_id }]}
182
- end
183
- Garner::Cache::ObjectIdentity.cache_multi(bindings) do |binding|
184
- # the object binding is passed into the block for every cache miss
185
- Model.find(binding[:bind][1][:id])
186
- end
187
- ```
188
161
 
189
162
  Configuration
190
163
  -------------
@@ -197,27 +170,35 @@ Garner.configure do |config|
197
170
  end
198
171
  ```
199
172
 
200
- Preventing Clients from Caching Dynamic Data
201
- --------------------------------------------
173
+ The full list of `Garner.config` attributes is:
202
174
 
203
- Generally, dynamic data cannot have a well-defined expiration time. Therefore, we must tell the client not to cache it. This can be accomplished using the `Garner::Middleware::Cache::Bust` middleware, executed after any API call. The middleware adds a `Cache-Control` and an `Expires` header.
204
-
205
- ```
206
- Cache-Control: private, max-age=0, must-revalidate
207
- Expires: Fri, 01 Jan 1990 00:00:00 GMT
175
+ * `:global_cache_options`: A hash of options to be passed on every call to `Garner.config.cache`, like `{ :expires_in => 10.minutes }`. Defaults to `{}`
176
+ * `:context_key_strategies`: An array of context key strategies, to be applied in order. Defaults to `[Garner::Strategies::Context::Key::Caller]`
177
+ * `:rack_context_key_strategies`: Rack-specific context key strategies. Defaults to:
178
+ ```ruby
179
+ [
180
+ Garner::Strategies::Context::Key::Caller,
181
+ Garner::Strategies::Context::Key::RequestGet,
182
+ Garner::Strategies::Context::Key::RequestPost,
183
+ Garner::Strategies::Context::Key::RequestPath
184
+ ]
208
185
  ```
209
-
210
- The `private` option of the `Cache-Control` header instructs the client that it is allowed to store data in a private cache (unnecessary, but is known to work around overzealous cache implementations), `max-age` that it must check with the server every time it needs this data and `must-revalidate` prevents gateways from returning a response if your API server is unreachable. An additional `Expires` header will make double-sure the entire request expires immediately.
186
+ * `:binding_key_strategy`: Binding key strategy. Defaults to `Garner::Strategies::Binding::Key::SafeCacheKey`.
187
+ * `: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
+ * `:mongoid_identity_fields`: Identity fields considered legal for the `identity` method. Defaults to `[:_id]`.
191
+ * `: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.
211
192
 
212
193
  Contributing
213
194
  ------------
214
195
 
215
- Fork the project. Make your feature addition or bug fix with tests. Send a pull request. Bonus points for topic branches.
196
+ Fork the project. Make your feature addition or bug fix with tests. Send a pull request.
216
197
 
217
198
  Copyright and License
218
199
  ---------------------
219
200
 
220
- MIT License, see [LICENSE](https://github.com/dblock/garner/blob/master/LICENSE.md) for details.
201
+ MIT License, see [LICENSE](LICENSE.md) for details.
221
202
 
222
- (c) 2012 [Art.sy Inc.](http://artsy.github.com), [Frank Macreery](https://github.com/macreery), [Daniel Doubrovkine](https://github.com/dblock) and [Contributors](https://github.com/dblock/garner/blob/master/CHANGELOG.md)
203
+ (c) 2012-2013 [Artsy](http://artsy.github.com), [Frank Macreery](https://github.com/fancyremarker), [Daniel Doubrovkine](https://github.com/dblock) and [contributors](CHANGELOG.md).
223
204
 
@@ -0,0 +1,58 @@
1
+ # Set up Garner configuration parameters
2
+ Garner.config.option(:binding_key_strategy, {
3
+ :default => Garner::Strategies::Binding::Key::SafeCacheKey
4
+ })
5
+ Garner.config.option(:binding_invalidation_strategy, {
6
+ :default => Garner::Strategies::Binding::Invalidation::Touch
7
+ })
8
+
9
+ module Garner
10
+ module Cache
11
+ module Binding
12
+
13
+ # Override this method to use a custom key strategy.
14
+ #
15
+ # @return [Object] The strategy to be used for instances of this class.
16
+ def key_strategy
17
+ Garner.config.binding_key_strategy
18
+ end
19
+
20
+ # Apply the cache key strategy to this binding.
21
+ def garner_cache_key
22
+ key_strategy.apply(self)
23
+ end
24
+
25
+ # Override this method to use a custom invalidation strategy.
26
+ #
27
+ # @return [Object] The strategy to be used for instances of this class.
28
+ def invalidation_strategy
29
+ Garner.config.binding_invalidation_strategy
30
+ end
31
+
32
+ # Apply the invalidation strategy to this binding.
33
+ def invalidate_garner_caches
34
+ invalidation_strategy.apply(self)
35
+ end
36
+
37
+ protected
38
+ def _garner_after_create
39
+ if invalidation_strategy.apply_on_callback?(:create)
40
+ invalidation_strategy.apply(self)
41
+ end
42
+ end
43
+
44
+ def _garner_after_update
45
+ if invalidation_strategy.apply_on_callback?(:update)
46
+ invalidation_strategy.apply(self)
47
+ end
48
+ end
49
+
50
+ def _garner_after_destroy
51
+ if invalidation_strategy.apply_on_callback?(:destroy)
52
+ invalidation_strategy.apply(self)
53
+ end
54
+ end
55
+
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,27 @@
1
+ # Set up Garner configuration parameters
2
+ Garner.config.option(:context_key_strategies, {
3
+ :default => [Garner::Strategies::Context::Key::Caller]
4
+ })
5
+
6
+ module Garner
7
+ module Cache
8
+ module Context
9
+
10
+ # Instantiate a context-appropriate cache identity.
11
+ #
12
+ # @example
13
+ # garner.bind(current_user) do
14
+ # { count: current_user.logins.count }
15
+ # end
16
+ # @return [Garner::Cache::Identity] The cache identity.
17
+ def garner(&block)
18
+ identity = Garner::Cache::Identity.new
19
+ Garner.config.context_key_strategies.each do |strategy|
20
+ identity = strategy.apply(identity, self)
21
+ end
22
+
23
+ block_given? ? identity.fetch(&block) : identity
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ module Garner
2
+ module Cache
3
+ class Identity
4
+ attr_accessor :bindings, :key_hash, :options_hash
5
+
6
+ def initialize
7
+ @bindings = []
8
+ @key_hash = {}
9
+
10
+ # Set up options hash with defaults
11
+ @options_hash = Garner.config.global_cache_options || {}
12
+ @options_hash.merge!({ :expires_in => Garner.config.expires_in })
13
+ end
14
+
15
+ def fetch(&block)
16
+ Garner::Cache.fetch(@bindings, @key_hash, @options_hash, &block)
17
+ end
18
+
19
+ # Bind this cache identity to a (bindable) object.
20
+ #
21
+ # @param object [Object] An object; should support configured binding strategy.
22
+ def bind(object, &block)
23
+ @bindings << object
24
+ block_given? ? fetch(&block) : self
25
+ end
26
+
27
+ # Merge the given hash into the cache identity's key hash.
28
+ #
29
+ # @param hash [Hash] A hash to merge on top of the current key hash.
30
+ def key(hash, &block)
31
+ @key_hash.merge!(hash)
32
+ block_given? ? fetch(&block) : self
33
+ end
34
+
35
+ # Merge the given hash into the cache identity's cache options.
36
+ # Any cache_options supported by Garner.config.cache may be passed.
37
+ #
38
+ # @param hash [Hash] Options to pass to Garner.config.cache.
39
+ def options(hash, &block)
40
+ @options_hash.merge!(hash)
41
+ block_given? ? fetch(&block) : self
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ module Garner
2
+ module Cache
3
+
4
+ # Fetch a result from cache.
5
+ #
6
+ # @param bindings [Array] Objects to which the the cache result should be
7
+ # bound. These objects' keys are injected into the compound cache key.
8
+ # @param key_hash [Hash] Hash to comprise the compound cache key.
9
+ # @param options_hash [Hash] Options to be passed to Garner.config.cache.
10
+ def self.fetch(bindings, key_hash, options_hash, &block)
11
+ if (compound_key = compound_key(bindings, key_hash))
12
+ result = Garner.config.cache.fetch(compound_key, options_hash) do
13
+ yield
14
+ end
15
+ Garner.config.cache.delete(compound_key) unless result
16
+ else
17
+ result = yield
18
+ end
19
+ result
20
+ end
21
+
22
+ private
23
+ def self.compound_key(bindings, key_hash)
24
+ binding_keys = bindings.map(&:garner_cache_key).compact
25
+
26
+ if binding_keys.size == bindings.size
27
+ # All bindings have non-nil cache keys, proceed.
28
+ {
29
+ :binding_keys => binding_keys,
30
+ :context_keys => key_hash
31
+ }
32
+ else
33
+ # A nil cache key was generated. Skip caching.
34
+ # TODO: Replace this ill-documented "nil to skip" behavior
35
+ # with exceptions on inability to generate a cache key.
36
+ nil
37
+ end
38
+ end
39
+
40
+ end
41
+ end
data/lib/garner/config.rb CHANGED
@@ -1,7 +1,6 @@
1
1
  module Garner
2
2
 
3
3
  class << self
4
-
5
4
  # Set the configuration options. Best used by passing a block.
6
5
  #
7
6
  # @example Set up configuration options.
@@ -9,7 +8,7 @@ module Garner
9
8
  # config.cache = Rails.cache
10
9
  # end
11
10
  #
12
- # @return [ Config ] The configuration object.
11
+ # @return [Config] The configuration object.
13
12
  def configure
14
13
  block_given? ? yield(Garner::Config) : Garner::Config
15
14
  end
@@ -21,10 +20,10 @@ module Garner
21
20
 
22
21
  # Current configuration settings.
23
22
  attr_accessor :settings
24
-
23
+
25
24
  # Default configuration settings.
26
25
  attr_accessor :defaults
27
-
26
+
28
27
  @settings = {}
29
28
  @defaults = {}
30
29
 
@@ -33,10 +32,10 @@ module Garner
33
32
  # @example Define the option.
34
33
  # Config.option(:cache, :default => nil)
35
34
  #
36
- # @param [ Symbol ] name The name of the configuration option.
37
- # @param [ Hash ] options Extras for the option.
35
+ # @param [Symbol] name The name of the configuration option.
36
+ # @param [Hash] options Extras for the option.
38
37
  #
39
- # @option options [ Object ] :default The default value.
38
+ # @option options [Object] :default The default value.
40
39
  def option(name, options = {})
41
40
  defaults[name] = settings[name] = options[:default]
42
41
 
@@ -54,23 +53,29 @@ module Garner
54
53
  end
55
54
  RUBY
56
55
  end
57
-
58
- # Returns the default cache store, either Rails.cache or an instance of ActiveSupport::Cache::MemoryStore.
56
+
57
+ # Returns the default cache store, either Rails.cache or an instance
58
+ # of ActiveSupport::Cache::MemoryStore.
59
59
  #
60
60
  # @example Get the default cache store
61
61
  # config.default_cache
62
62
  #
63
- # @return [ Cache ] The default cache store instance.
63
+ # @return [Cache] The default cache store instance.
64
64
  def default_cache
65
- defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ::ActiveSupport::Cache::MemoryStore.new
65
+ if defined?(Rails) && Rails.respond_to?(:cache)
66
+ Rails.cache
67
+ else
68
+ ::ActiveSupport::Cache::MemoryStore.new
69
+ end
66
70
  end
67
71
 
68
- # Returns the cache, or defaults to Rails cache when running in Rails or an instance of ActiveSupport::Cache::MemoryStore otherwise.
72
+ # Returns the cache, or defaults to Rails cache when running in Rails
73
+ # or an instance of ActiveSupport::Cache::MemoryStore otherwise.
69
74
  #
70
75
  # @example Get the cache.
71
76
  # config.cache
72
77
  #
73
- # @return [ Cache ] The configured cache or a default cache instance.
78
+ # @return [Cache] The configured cache or a default cache instance.
74
79
  def cache
75
80
  settings[:cache] = default_cache unless settings.has_key?(:cache)
76
81
  settings[:cache]
@@ -81,11 +86,34 @@ module Garner
81
86
  # @example Set the cache.
82
87
  # config.cache = Rails.cache
83
88
  #
84
- # @return [ Cache ] The newly set cache.
89
+ # @return [Cache] The newly set cache.
85
90
  def cache=(cache)
86
91
  settings[:cache] = cache
87
92
  end
88
93
 
94
+ # Returns the default caller root, as determined by
95
+ # Garner::Strategies::Context::Key::Caller.
96
+ #
97
+ # @return [String] The default caller_root path.
98
+ def default_caller_root
99
+ Garner::Strategies::Context::Key::Caller.default_root
100
+ end
101
+
102
+ # Returns the manually configured caller_root, or a default.
103
+ #
104
+ # @return [String] The configured caller_root or a default.
105
+ def caller_root
106
+ settings[:caller_root] = default_caller_root unless settings.has_key?(:caller_root)
107
+ settings[:caller_root]
108
+ end
109
+
110
+ # Sets the caller_root to use.
111
+ #
112
+ # @return [String] The newly set caller_root.
113
+ def caller_root=(caller_root)
114
+ settings[:caller_root] = caller_root
115
+ end
116
+
89
117
  # Reset the configuration options to the defaults.
90
118
  #
91
119
  # @example Reset the configuration options.
@@ -93,7 +121,10 @@ module Garner
93
121
  def reset!
94
122
  settings.replace(defaults)
95
123
  end
96
-
124
+
125
+ # Default cache options
126
+ option(:global_cache_options, :default => {})
127
+
97
128
  # Default cache expiration time.
98
129
  option(:expires_in, :default => nil)
99
130
  end