Garner
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!
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 at GoRuCo 2012.
Upgrading
The current stable release line of Garner is 0.5.x, and contains many breaking changes from the previous 0.3.x series. For a summary of important changes, see UPGRADING.
Usage
Application Logic Caching
Add Garner to your Gemfile with gem "garner"
and run bundle install
. Next, include the appropriate mixin in your app:
- For plain-old Ruby apps,
include Garner::Cache::Context
. - For Rack apps, first
require "garner/mixins/rack"
, theninclude Garner::Mixins::Rack
. (This provides saner defaults for injecting request parameters into the cache context key. More on cache context keys later.)
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.
get "/system/counts/all" do
# Compute once and cache for subsequent reads
garner do
{
"orders_count" => Order.count,
"users_count" => User.count
}
end
end
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.
get "/me/address" do
# Invalidate when current_user is modified
garner.bind(current_user) do
current_user.address
end
end
ORM Integrations
Mongoid
To use Mongoid 3, 4 or 5 documents and classes for Garner bindings, use Garner::Mixins::Mongoid::Document
. You can set it up in an initializer:
require "garner/mixins/mongoid"
module Mongoid
module Document
include Garner::Mixins::Mongoid::Document
end
end
This enables binding to Mongoid classes as well as instances. For example:
get "/system/counts/orders" do
# Invalidate when any order is created, updated or deleted
garner.bind(Order) do
{
"orders_count" => Order.count,
}
end
end
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:
get "/order/:id" do
# Invalidate when Order.find(params[:id]) is modified
garner.bind(Order.identify(params[:id])) do
Order.find(params[:id])
end
end
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.:
Garner.configure do |config|
config.mongoid_identity_fields = [:_id, :_slugs]
end
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.
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.
order = Order.garnered_find(3)
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:
Order.invalidate_garner_caches
Order.find(3).invalidate_garner_caches
ActiveRecord
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:
require "garner/mixins/active_record"
module ActiveRecord
class Base
include Garner::Mixins::ActiveRecord::Base
end
end
Cache Options
You can pass additional options directly to the cache implementation:
get "/latest_order" do
# Expire the latest order every 15 minutes
garner.options(expires_in: 15.minutes) do
Order.latest
end
end
Under The Hood: Bindings
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:
get "/system/counts/all" do
# Invalidate when any order or user is modified
garner.bind(Order).bind(User) do
{
"orders_count" => Order.count,
"users_count" => User.count
}
end
end
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.
Additional binding and invalidation strategies can be written. To use them, set Garner.config.binding_key_strategy
and Garner.config.binding_invalidation_strategy
.
Under The Hood: Cache Context Keys
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:
Garner::Strategies::Context::Key::Caller
inserts the calling file and line number, allowing multiple calls from the same function to generate different results.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.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.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.
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:
get "/order/:id" do
garner.bind(Order.identify(params[:id])).key({ role: current_user.role }) do
Order.find(params[:id])
end
end
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.
Configuration
By default Garner
will use an instance of ActiveSupport::Cache::MemoryStore
in a non-Rails and Rails.cache
in a Rails environment. You can configure it to use any other cache store.
Garner.configure do |config|
config.cache = ActiveSupport::Cache::FileStore.new
end
The full list of Garner.config
attributes is:
:global_cache_options
: A hash of options to be passed on every call toGarner.config.cache
, like{ :expires_in => 10.minutes }
. Defaults to{}
:context_key_strategies
: An array of context key strategies, to be applied in order. Defaults to[Garner::Strategies::Context::Key::Caller]
:rack_context_key_strategies
: Rack-specific context key strategies. Defaults to:
[
Garner::Strategies::Context::Key::Caller,
Garner::Strategies::Context::Key::RequestGet,
Garner::Strategies::Context::Key::RequestPost,
Garner::Strategies::Context::Key::RequestPath
]
:binding_key_strategy
: Binding key strategy. Defaults toGarner::Strategies::Binding::Key::SafeCacheKey
.:binding_invalidation_strategy
: Binding invalidation strategy. Defaults toGarner::Strategies::Binding::Invalidation::Touch
.:mongoid_identity_fields
: Identity fields considered legal for theidentity
method. Defaults to[:_id]
.:caller_root
: Root path of application, to be stripped out of value strings generated by theCaller
context key strategy. Defaults toRails.root
if in a Rails environment; otherwise to the nearest ancestor directory containing a Gemfile.:invalidate_mongoid_root
: If set to true, invalidates the_root
document along with any embedded Mongoid document binding. Defaults totrue
.:whiny_nils
: If set to true, raises an exception when anil
binding is specified (i.e.,garner.bind(nil)
). Defaults totrue
.
Contributing
Fork the project. Make your feature addition or bug fix with tests. Send a pull request.
Copyright and License
MIT License, see LICENSE for details.
(c) 2012-2013 Artsy, Frank Macreery, Daniel Doubrovkine and contributors.