garner 0.3.3 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE.md +1 -1
- data/README.md +116 -135
- data/lib/garner/cache/binding.rb +58 -0
- data/lib/garner/cache/context.rb +27 -0
- data/lib/garner/cache/identity.rb +45 -0
- data/lib/garner/cache.rb +41 -0
- data/lib/garner/config.rb +46 -15
- data/lib/garner/mixins/mongoid/document.rb +75 -0
- data/lib/garner/mixins/mongoid/identity.rb +106 -0
- data/lib/garner/mixins/mongoid.rb +4 -0
- data/lib/garner/mixins/rack.rb +45 -0
- data/lib/garner/strategies/binding/invalidation/base.rb +26 -0
- data/lib/garner/strategies/binding/invalidation/touch.rb +27 -0
- data/lib/garner/strategies/binding/key/base.rb +19 -0
- data/lib/garner/strategies/binding/key/cache_key.rb +19 -0
- data/lib/garner/strategies/binding/key/safe_cache_key.rb +33 -0
- data/lib/garner/strategies/context/key/base.rb +21 -0
- data/lib/garner/strategies/context/key/caller.rb +83 -0
- data/lib/garner/strategies/context/key/jsonp.rb +30 -0
- data/lib/garner/strategies/context/key/request_get.rb +30 -0
- data/lib/garner/strategies/context/key/request_path.rb +28 -0
- data/lib/garner/strategies/context/key/request_post.rb +30 -0
- data/lib/garner/version.rb +1 -1
- data/lib/garner.rb +29 -26
- metadata +122 -22
- data/lib/garner/cache/object_identity.rb +0 -249
- data/lib/garner/middleware/base.rb +0 -47
- data/lib/garner/middleware/cache/bust.rb +0 -20
- data/lib/garner/mixins/grape_cache.rb +0 -111
- data/lib/garner/mixins/mongoid_document.rb +0 -58
- data/lib/garner/strategies/cache/expiration_strategy.rb +0 -16
- data/lib/garner/strategies/etags/grape_strategy.rb +0 -32
- data/lib/garner/strategies/etags/marshal_strategy.rb +0 -16
- data/lib/garner/strategies/keys/caller_strategy.rb +0 -38
- data/lib/garner/strategies/keys/jsonp_strategy.rb +0 -24
- data/lib/garner/strategies/keys/key_strategy.rb +0 -21
- data/lib/garner/strategies/keys/request_get_strategy.rb +0 -24
- data/lib/garner/strategies/keys/request_path_strategy.rb +0 -21
- data/lib/garner/strategies/keys/request_post_strategy.rb +0 -24
- 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
|
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
|
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
|
-
|
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
|
-
|
9
|
-
|
8
|
+
Stable Release
|
9
|
+
--------------
|
10
10
|
|
11
|
-
|
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
|
-
|
14
|
+
Usage
|
15
|
+
-----
|
14
16
|
|
15
|
-
|
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
|
-
|
19
|
+
Add Garner to your Gemfile with `gem "garner"` and run `bundle install`. Next, include the appropriate mixin in your app:
|
23
20
|
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
37
|
-
|
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
|
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
|
-
|
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
|
-
|
50
|
+
ORM Integrations
|
51
|
+
----------------
|
58
52
|
|
59
|
-
|
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
|
-
|
65
|
+
This enables binding to Mongoid classes as well as instances. For example:
|
99
66
|
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
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
|
-
|
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
|
-
```
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
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
|
-
|
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
|
-
|
139
|
-
|
107
|
+
```ruby
|
108
|
+
Order.invalidate_garner_caches
|
109
|
+
Order.find(3).invalidate_garner_caches
|
110
|
+
```
|
140
111
|
|
141
|
-
|
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
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
201
|
-
--------------------------------------------
|
173
|
+
The full list of `Garner.config` attributes is:
|
202
174
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
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
|
-
|
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.
|
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](
|
201
|
+
MIT License, see [LICENSE](LICENSE.md) for details.
|
221
202
|
|
222
|
-
(c) 2012 [
|
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
|
data/lib/garner/cache.rb
ADDED
@@ -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 [
|
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 [
|
37
|
-
# @param [
|
35
|
+
# @param [Symbol] name The name of the configuration option.
|
36
|
+
# @param [Hash] options Extras for the option.
|
38
37
|
#
|
39
|
-
# @option options [
|
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
|
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 [
|
63
|
+
# @return [Cache] The default cache store instance.
|
64
64
|
def default_cache
|
65
|
-
defined?(Rails) && Rails.respond_to?(:cache)
|
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
|
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 [
|
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 [
|
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
|