Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
A Ruby on Rails framework to support cache management based on explicitely modeled caching dependencies.

This branch is even with einzige:master

Fetching latest commit…

Cannot retrieve the latest commit at this time

Failed to load latest commit information.
db
lib
spec
.gitignore
.travis.yml
Gemfile
Gemfile.lock
LICENSE.txt
README.md
Rakefile
VERSION
cache-machine.gemspec

README.md

cache-machine

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

  1. Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
  2. Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
  3. Fork the project
  4. Start a feature/bugfix branch
  5. Commit and push until you are happy with your contribution
  6. Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
  7. 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.

Something went wrong with that request. Please try again.