• Stars
    star
    159
  • Rank 235,231 (Top 5 %)
  • Language
    Ruby
  • License
    MIT License
  • Created almost 14 years ago
  • Updated about 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Tag based caching for Rails using Redis. Associate different cached content with a tag, then expire by tag instead of key.

Cashier: Tag Based Caching for Rails

Build Status Gem Version Code Climate Dependency Status

Manage your cache keys with tags, forget about keys!

What Is It?

# in your view
cache @some_record, :tag => 'some-component'

# in another view
cache @some_releated_record, :tag => 'some-component'

# can have multiple tags
cache @something, :tag => ['dashboard', 'settings'] # can expire from either tag

# in an observer
Cashier.expire 'some-component' # don't worry about keys! Much easier to sweep with confidence

# in your controller
caches_action :tag => 'complicated-action', :cache_path => proc { |c| 
  # huge complicated mess of parameters
  c.params
}

# need to access the controller?
caches_action :tag => proc {|c|
  # c is the controller
  "users/#{c.current_user.id}/dashboard"      
}

# in your sweeper, in your observers, in your Resque jobs...wherever
Cashier.expire 'complicated-action'
Cashier.expire 'tag1', 'tag2', 'tag3', 'tag4'

# It integrates smoothly with Rails.cache as well, not just the views
Rails.cache.fetch("user_1", :tag => ["users"]) { User.find(1) }
Rails.cache.fetch("user_2", :tag => ["users"]) { User.find(2) }
Rails.cache.fetch("user_3", :tag => ["users"]) { User.find(3) }
Rails.cache.fetch("admins", :tag => ["users"]) { User.where(role: "Admin").all }

# You can then expire all your users 
Cashier.expire "users"

# You can also use Rails.cache.write
Rails.cache.write("foo", "bar", :tag => ["some_tag"])

# what's cached
Cashier.tags

# Clears out all tagged keys and tags
Cashier.clear

How it Came About

I work on an application that involves all sorts of caching. I try to use action caching whenever I possible. I had an index action that had maybe ~20 different combination of filters and sorting. If you want to use action caching you have to create a unique key for every combination. This created a nice 6 nested loop to expire the cache. Once you had pagination, then you have even more combinations of possible cache keys. I needed a better solution. I wanted to expire things logically as a viewed them on the page. IE, if a record was added, I wanted to say "expire that page". Problem was that page contained ~1000 different keys. So I needed something to store the keys for me and associate them with tags. That's exactly what cashier does. Cache associate individual cache keys with a tag, then expire them all at once. This took my 7 layer loop down to one line of code. It's also made managing the cache throught my application much easier.

Why Tag Based Caching is Useful

  1. You don't worry about keys. How many times have you created a complicated key for a fragment or action then messed up when you tried to expire the cache
  2. Associate your cached content into groups of related content. If you have records that are closely associated or displayed together, then you can tag them and expire them at once.
  3. Expire cached content from anywhere. If you've done any serious development, you know that Rails caching does not work (easily) outside the scope of an HTTP request. If you have background jobs that manipulate data or potentially invalidate cached data, you know how much of a pain it is to say expire_fragment in some random code.
  4. Don't do anything differently! All you have to do is pass :tag => 'something' into cache (in the view) or caches_action in the controller.

How it Works

Cashier hooks into Rails' store_fragment method using alias_method_chain to run some code that captures the key and tag then stores that in the rails cache.

Adapters

Cashier has 2 adapters for the tags storing, :cache_store or :redis_store.

IMPORTANT: this store is ONLY for the tags, your fragments will still be stored in Rails.cache.

Setting an adapter for working with the cache as the tags storage

# config/environment/production.rb

config.cashier.adapter = :cache_store 
# or config.cashier.adapter = :redis_store

Setting an adapter for working with Redis as the tags storage

# config/environment/production.rb
config.cashier.adapter.redis = Redis.new(:host => '127.0.0.1', :port => '3697') # or Resque.redis or any existing redis connection

Why Redis?

The reason Redis was introduced is that while the Rails.cache usage for the tags store is clean and involves no "outer" dependencies, since memcached is limited to read/write, it can slow down the application quite a bit.

If you work with very large arrays of keys and tags, you may see slowness in the cache communication.

Redis was introduces since it has the ability to work with "sets", and you can add/remove tags from this set without reading the entire array.

Benchmarking

Using the cache adapter, this piece of code takes 3 seconds on average

Benchmark.measure do
  500.times do
    key = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
    tag = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
    tag2 = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
    Cashier.store_fragment(key, tag, tag2)
  end
end

Using the Redis adapter, the same piece of code takes 0.8 seconds, quite the difference :)

Notifications

Cashier will send out events when things happen inside the library. The events are sent out through ActiveSupport::Notifications so you can pretty much subscribe to the events from anywhere you want.

Here are the way you can subscribe to the events and use the data from them.

# Subscribe to the store fragment event, this is fired every time cashier will call the "store_fragment" method
# payload[:data] will be something like this: ["key", ["tag1", "tag2", "tag3"]]
ActiveSupport::Notifications.subscribe("store_fragment.cashier") do |name, start, finish, id, payload|
		
end

# Subscribe to the expire event.
# payload[:data] will be the list of tags expired.
ActiveSupport::Notifications.subscribe("expire.cashier") do |name, start, finish, id, payload|
    
end 

# Subscribe to the clear event. (no data)
ActiveSupport::Notifications.subscribe("clear.cashier") do |name, start, finish, id, payload|
    
end 

# Subscribe to the delete_cache_key event
# this event will fire every time there's a Rails.cache.delete with the key
# payload[:data] will be the key name that's been deleted from the cache
ActiveSupport::Notifications.subscribe("delete_cache_key.cashier") do |name, start, finish, id, payload|
		
end	

# Subscribe to the o_write_cache_key event
# this event will fire every time there's a Rails.cache.write with the key
# payload[:data] will be the key name that's been written to the cache
ActiveSupport::Notifications.subscribe("write_cache_key.cashier") do |name, start, finish, id, payload|
		
end		

Notifications use case

At Gogobot we have a plugin to invalidate the external CDN cache on full pages for logged out users. The usage is pretty unlimited.

If you think we're missing a notification, please do open an issue or be awesome and do it yourself and open a pull request.

Contributors

  • twinturbo - Initial Implementation
  • KensoDev - Adding Redis support (Again \o/)
  • KensoDev - Adding plugins support for callback methods

Contributing to Cashier

  • 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) 2010 Adam Hawkins. See LICENSE.txt for further details.

More Repositories

1

harness

Ruby
174
star
2

chassis

Ruby
90
star
3

active_model_serializers-matchers

RSpec matchers for ActiveModel::Serializers
Ruby
40
star
4

api_guides

Ruby
30
star
5

emberaddons

Source for http://emberaddons.com
Ruby
30
star
6

interchange

Facades around interchangeable objects & system boundaries
Ruby
30
star
7

breaker

Circuit breaker pattern for well designed Ruby applications
Ruby
28
star
8

hawkins.io

HTML
19
star
9

logger-better

Simple enhancements for the Ruby stdlib Logger
Ruby
19
star
10

docker-project-template

Boilerplate for building docker based projects
Shell
19
star
11

chassis-example

How to Chassis
Ruby
18
star
12

Unobtrusive-Login

Example of creating an unobtrusive login system using Prototype and jQuery
Ruby
18
star
13

docker-thrift

Dockerfile
17
star
14

tnt

Make understandable and actionable error classes easily
Ruby
16
star
15

vagrant-workstation

CLI for encapsulating multiple projects inside single VM
Shell
12
star
16

manifold

Never worry about CORS again
Ruby
11
star
17

dotfiles

das config
Shell
11
star
18

semaphore-continuous-deployment

Ruby
10
star
19

applications-first-frameworks-second

For My Railsconf 2014 Workshop
Ruby
7
star
20

gemfury_helpers

Bundler::GemHelper for Gemfury
Ruby
7
star
21

billing

Ruby
7
star
22

ember-dev-example

How to build an ember package using ember-dev
JavaScript
6
star
23

newrelic-middleware

Track execution time of your middleware stack
Ruby
6
star
24

barcelona

Ruby
6
star
25

Nettuts-Capistrano-Deployments

Source Code for Screencast
Ruby
6
star
26

comp

Automation for configuring my machines with Ansible
Shell
5
star
27

rails_log_stdout

Log all rails related things to STDOUT
Ruby
5
star
28

harness-rack

Ruby
5
star
29

whatgem

Gem ranking website
Ruby
5
star
30

Nettuts-Zero-to-Sixty

Source for an article I wrote
4
star
31

sproutcore-login-tutorial

JavaScript
4
star
32

sunspot_search

Ruby
4
star
33

tweet_clock

Schedule Tweets to go out at multiple times in any time zone.
Ruby
4
star
34

semaphore-gke-tutorial

Shell
4
star
35

http_log

Log HTTP requests to MongoDB for debugging and access them via the web
CSS
4
star
36

opa-kubernetes

Open Policy Agent for validating Kubernetes manifests prior to deploying
Shell
4
star
37

billing-rails

Using Billing inside your rails app
Ruby
4
star
38

spray_paint

Simplest possible tagging for ActiveRecord
Ruby
3
star
39

field_notes

Ruby
3
star
40

resque-web-assets-test

Ruby
2
star
41

sunspot_search_demo

Demo app showing off the cool stuff you can do with sunspot_search
Ruby
2
star
42

semaphore-packer-tutorial

Shell
2
star
43

country_codes

Lookup country related metadata.
Ruby
2
star
44

serpentine

Ruby
2
star
45

mail-xoauth

Gmail OAuth IMAP & SMTP connections for the Mail gem.
Ruby
2
star
46

bindir

Shell
2
star
47

interview-questions

Ruby
1
star
48

talks

Ruby
1
star
49

ams_rails326_issue

Ruby
1
star
50

buildkite-kubernetes-tutorial

Shell
1
star
51

buildkite-ecs-tutorial

Shell
1
star
52

isos

HTML
1
star
53

website

Source for hawkins.io
HTML
1
star
54

boost

Ruby
1
star
55

lint-staged-shellcheck

Shell
1
star
56

rack-no_robots

Ruby
1
star
57

habtm_finder_bug

Ruby
1
star
58

harness-activesupport

Ruby
1
star
59

dyno

Ruby
1
star
60

rack-bounce

Ruby
1
star
61

harness-haproxy

Ruby
1
star
62

raa-screencast

Ruby
1
star
63

counter_cache_test

Ruby
1
star