An ActiveRecord mixin that helps managing cached content in a Ruby on Rails application with complex data update dependencies.
Cache Machine provides:
- high-level methods for accessing cached content using page names, numbers, time stamps etc,
- a DSL to describe update dependencies between the data models underlying the cached content,
- automatic cache invalidation based on those explicitly modeled data update dependencies.
You will find Cache Machine useful if you:
- use Memcache to cache fragments of a web site that contain data from a variety of underlying data models
- anytime one of the underlying data models changes, all the cached page fragments in which this data model occurs - and only those - need to be invalidated/updated
- you have many data models, cached fragments, and many data models used inside each cached fragment
- you want to update cache from background job (i.e. cache-sweeper does not know about your changes)
Cache Machine is library agnostic. You can use your own cache adapters (see below).
Usage
Setup your cache dependencies in config/initializers/cache-machine.rb using cache map. Very similar to Rails routes:
CacheMachine::Cache::Map.new.draw do
resource City do
collection :streets do
member :houses
end
collection :houses do
member :bricks
member :windows
end
end
resource Street do
collection :houses
collection :walls
end
resource House do
collection :walls, :scope => :vertical, :timestamp => false do
members :front_walls, :side_walls
member :bricks
member :windows
end
end
end
In this case your models should look like this:
class City < ActiveRecord::Base
has_many :streets
has_many :houses, :through => :streets
end
class Street < ActiveRecord::Base
belongs_to :city
has_many :houses
has_many :walls, :through => :houses
end
class House < ActiveRecord::Base
belongs_to :street
has_many :walls
end
class Wall < ActiveRecord::Base
belongs_to :house
# has_many :bricks
end
This example shows you how changes in your database affect on cache:
- When you create/update/destroy any wall:
- cache of walls collection expired for house associated with that updated/created/destroyed wall
- cache of walls collection expired for street (where wall's house is located) associated with that updated/created/destroyed
- cache of front_walls and side_walls expired for house associated with that updated/created/destroyed wall
- cache of bricks expired for house associated with that updated/created/destroyed wall
- cache of windows expired for house associated with that updated/created/destroyed wall
- When you create/update/destroy any house:
- cache of houses updated for associated street
- cache of houses updated for associated city
- When you create/update/destroy any street:
- cache of streets updated for associated city
- cache of houses updated for associated city
- ... :)
Member may have any name, whatever you want. But invalidation process starts only when collection is changed.
Custom cache invalidation
Using timestamps
Timestamps allow you to build very complex and custom cache dependencies.
In your model:
class House < ActiveRecord::Base
define_timestamp(:walls_timestamp) { [ bricks.count, windows.last.updated_at ] }
end
Anywhere else:
@house.fetch_cache_of :walls, :timestamp => :walls_timestamp do
walls.where(:built_at => Date.today)
end
This way you add additional condition to cache-key used for fetching data from cache: Any time when bricks count is changed or any window is updated your cache key will be changed and block will return fresh data. Timestamp should return array or string.
Using Cache Machine timestamps
Suppose you need to reset cache of tweets every 10 minutes.
class LadyGaga < ActiveRecord::Base
define_timestamp :tweets_timestamp, :expires_in => 10.minutes do
...
end
end
#...
# Somewhere
@lady_gaga.fetch_cache_of :tweets, :timestamp => :tweets_timestamp do
TwitterApi.fetch_tweets_for @lady_gaga
end
fetch_cache_of
block uses same options as Rails.cache.fetch. You can easily add expires_in option in it directly.
@house.fetch_cache :bricks, :expires_in => 1.minute do
...
end
Cache Machine stores timestamps for each of your model declared as resource in cache map.
House.timestamp
Each time your houses collection is changed timestamp will change its value. You can disable this callback in your cache map:
CacheMachine::Cache::Map.new.draw do
resource House, :timestamp => false
end
Manual cache invalidation
# For classes.
House.reset_timestamp
# For collections.
@house.delete_cache :bricks
# For timestamps.
@house.reset_timestamp :bricks
# You can reset all associated caches using map.
@house.delete_all_caches
Associations cache
You can fetch ids of an association from cache.
@house.association_ids(:bricks) # will return array of ids
You can fetch associated objects from cache.
@house.associated_from_cache(:bricks) # will return scope of relation with condition to ids from cache map.
ActionView helper
From examples above:
<%= cache_for @lady_gaga, :upcoming_events, :timestamp => :each_hour do %>
<p>Don't hide yourself in regret
Just love yourself and you're set</p>
<% end %>
Adapters
Cache Machine supports different types for storing cache:
- cache map adapter contains ids of relationships for each object from cache map
- timestamps adapter contains timestamps
- content (storage) adapter contains cached content itself (usually strings, html, etc)
You can setup custom adapters in your environment:
url = "redis://user:pass@host.com:9383/"
uri = URI.parse(url)
CacheMachine::Cache.timestamps_adapter = CacheMachine::Adapters::Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
CacheMachine::Cache.storage_adapter = CacheMachine::Adapters::Rails.new
CacheMachine::Cache.map_adapter = CacheMachine::Adapters::Rails.new
Default adapter uses standard Rails.cache
API.
Redis adapter is available in cache-machine-redis gem, please check out here.
Rake tasks
Cache machine will produce SQL queries on each update in collection until all map of associations will stored in cache. You can "prefill" cache map running:
rake cache_machine:fill_associations_map
Rake accepts model names as params. Each of these models must be defined as resource in cache map:
CacheMachine::Cache::Map.new.draw do
resource House
resource Wall
end
rake cache_machine:fill_associations_map[House, Wall]
If all objects from database is too much for you, add additional scope to resource or collection:
CacheMachine::Cache::Map.new.draw do
resource House, :scope => :offices do
collection :bricks, :scope => :squared
end
resource Wall
end
Contributing to cache-machine
- Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
- Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
- Fork the project
- Start a feature/bugfix branch
- Commit and push until you are happy with your contribution
- Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
- Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
Copyright
Copyright (c) 2011 PartyEarth LLC. See LICENSE.txt for details.