cashier 0.2.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -42
- data/README.md +183 -0
- data/Rakefile +1 -5
- data/cashier.gemspec +8 -11
- data/lib/cashier.rb +93 -35
- data/lib/cashier/adapters/cache_store.rb +45 -0
- data/lib/cashier/adapters/redis_store.rb +46 -0
- data/lib/cashier/application_controller.rb +28 -0
- data/lib/cashier/matchers.rb +0 -1
- data/lib/cashier/railtie.rb +4 -3
- data/lib/cashier/version.rb +1 -1
- data/spec/{application_controller_spec.rb → controllers/application_controller_spec.rb} +0 -8
- data/spec/{test_app → dummy}/.gitignore +0 -0
- data/spec/{test_app → dummy}/Gemfile +0 -0
- data/spec/{test_app → dummy}/README +0 -0
- data/spec/{test_app → dummy}/Rakefile +0 -0
- data/spec/{test_app → dummy}/app/controllers/application_controller.rb +0 -0
- data/spec/{test_app → dummy}/app/controllers/home_controller.rb +0 -0
- data/spec/{test_app → dummy}/app/helpers/application_helper.rb +0 -0
- data/spec/{test_app → dummy}/app/helpers/home_helper.rb +0 -0
- data/spec/{test_app → dummy}/app/views/home/index.html.erb +0 -0
- data/spec/{test_app → dummy}/app/views/layouts/application.html.erb +0 -0
- data/spec/{test_app → dummy}/config.ru +0 -0
- data/spec/{test_app → dummy}/config/application.rb +0 -4
- data/spec/{test_app → dummy}/config/boot.rb +0 -0
- data/spec/{test_app → dummy}/config/environment.rb +0 -0
- data/spec/{test_app → dummy}/config/environments/development.rb +0 -3
- data/spec/{test_app → dummy}/config/environments/production.rb +0 -3
- data/spec/{test_app → dummy}/config/environments/test.rb +1 -6
- data/spec/{test_app → dummy}/config/initializers/backtrace_silencers.rb +0 -0
- data/spec/{test_app → dummy}/config/initializers/inflections.rb +0 -0
- data/spec/{test_app → dummy}/config/initializers/mime_types.rb +0 -0
- data/spec/{test_app → dummy}/config/initializers/secret_token.rb +0 -0
- data/spec/{test_app → dummy}/config/initializers/session_store.rb +0 -0
- data/spec/{test_app → dummy}/config/locales/en.yml +0 -0
- data/spec/{test_app → dummy}/config/routes.rb +0 -0
- data/spec/{test_app → dummy}/db/seeds.rb +0 -0
- data/spec/{test_app → dummy}/lib/tasks/.gitkeep +0 -0
- data/spec/{test_app → dummy}/public/404.html +0 -0
- data/spec/{test_app → dummy}/public/422.html +0 -0
- data/spec/{test_app → dummy}/public/500.html +0 -0
- data/spec/{test_app → dummy}/public/favicon.ico +0 -0
- data/spec/{test_app → dummy}/public/images/rails.png +0 -0
- data/spec/{test_app → dummy}/public/javascripts/application.js +0 -0
- data/spec/{test_app → dummy}/public/javascripts/controls.js +0 -0
- data/spec/{test_app → dummy}/public/javascripts/dragdrop.js +0 -0
- data/spec/{test_app → dummy}/public/javascripts/effects.js +0 -0
- data/spec/{test_app → dummy}/public/javascripts/prototype.js +0 -0
- data/spec/{test_app → dummy}/public/javascripts/rails.js +0 -0
- data/spec/{test_app → dummy}/public/robots.txt +0 -0
- data/spec/{test_app → dummy}/public/stylesheets/.gitkeep +0 -0
- data/spec/{test_app → dummy}/script/rails +0 -0
- data/spec/{test_app → dummy}/test/performance/browsing_test.rb +0 -0
- data/spec/{test_app → dummy}/test/test_helper.rb +0 -0
- data/spec/{test_app → dummy}/vendor/plugins/.gitkeep +0 -0
- data/spec/lib/adapters/cache_store_spec.rb +75 -0
- data/spec/lib/adapters/redis_store_spec.rb +89 -0
- data/spec/lib/cashier_spec.rb +112 -0
- data/spec/spec_helper.rb +34 -8
- metadata +179 -214
- data/..gemspec +0 -21
- data/.infinity_test +0 -19
- data/.rvmrc +0 -1
- data/Gemfile.lock +0 -118
- data/lib/cashier/controller_helper.rb +0 -34
- data/readme.md +0 -120
- data/spec/cashier_spec.rb +0 -89
- data/spec/test_app/Gemfile.lock +0 -73
- data/spec/test_app/config/database.yml +0 -22
data/.gitignore
CHANGED
@@ -1,42 +1,3 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
# rdoc generated
|
5
|
-
rdoc
|
6
|
-
|
7
|
-
# yard generated
|
8
|
-
doc
|
9
|
-
.yardoc
|
10
|
-
|
11
|
-
# bundler
|
12
|
-
.bundle
|
13
|
-
|
14
|
-
# jeweler generated
|
15
|
-
pkg
|
16
|
-
|
17
|
-
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
18
|
-
#
|
19
|
-
# * Create a file at ~/.gitignore
|
20
|
-
# * Include files you want ignored
|
21
|
-
# * Run: git config --global core.excludesfile ~/.gitignore
|
22
|
-
#
|
23
|
-
# After doing this, these files will be ignored in all your git projects,
|
24
|
-
# saving you from having to 'pollute' every project you touch with them
|
25
|
-
#
|
26
|
-
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
27
|
-
#
|
28
|
-
# For MacOS:
|
29
|
-
#
|
30
|
-
.DS_Store
|
31
|
-
#
|
32
|
-
# For TextMate
|
33
|
-
#*.tmproj
|
34
|
-
#tmtags
|
35
|
-
#
|
36
|
-
# For emacs:
|
37
|
-
#*~
|
38
|
-
#\#*
|
39
|
-
#.\#*
|
40
|
-
#
|
41
|
-
# For vim:
|
42
|
-
*.swp
|
1
|
+
spec/dummy/tmp/pids
|
2
|
+
spec/dummy/tmp/cache
|
3
|
+
Gemfile.lock
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# Cashier: Tag Based Caching for Rails
|
2
|
+
|
3
|
+
Manage your cache keys with tags, forget about keys!
|
4
|
+
|
5
|
+
## What Is It?
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# in your view
|
9
|
+
cache @some_record, :tag => 'some-component'
|
10
|
+
|
11
|
+
# in another view
|
12
|
+
cache @some_releated_record, :tag => 'some-component'
|
13
|
+
|
14
|
+
# can have multiple tags
|
15
|
+
cache @something, :tag => ['dashboard', 'settings'] # can expire from either tag
|
16
|
+
|
17
|
+
# in an observer
|
18
|
+
Cashier.expire 'some-component' # don't worry about keys! Much easier to sweep with confidence
|
19
|
+
|
20
|
+
# in your controller
|
21
|
+
caches_action :tag => 'complicated-action', :cache_path => proc { |c|
|
22
|
+
# huge complicated mess of parameters
|
23
|
+
c.params
|
24
|
+
}
|
25
|
+
|
26
|
+
# need to access the controller?
|
27
|
+
caches_action :tag => proc {|c|
|
28
|
+
# c is the controller
|
29
|
+
"users/#{c.current_user.id}/dashboard"
|
30
|
+
}
|
31
|
+
|
32
|
+
# in your sweeper, in your observers, in your resque jobs...wherever
|
33
|
+
Cashier.expire 'complicated-action'
|
34
|
+
Cashier.expire 'tag1', 'tag2', 'tag3', 'tag4'
|
35
|
+
|
36
|
+
# what's cached
|
37
|
+
Cashier.tags
|
38
|
+
|
39
|
+
# sweep all stored keys
|
40
|
+
Cashier.clear
|
41
|
+
```
|
42
|
+
|
43
|
+
## How it Came About
|
44
|
+
|
45
|
+
I work on an application that involves all sorts of caching. I try to use action caching whenever I possible.
|
46
|
+
I had an index action that had maybe ~20 different combination of filters and sorting. If you want to use
|
47
|
+
action caching you have to create a **unique** key for every combination. This created a nice 6 nested loop
|
48
|
+
to expire the cache. Once you had pagination, then you have even more combinations of possible cache keys.
|
49
|
+
I needed a better solution. I wanted to expire things logically as a viewed them on the page. IE, if
|
50
|
+
a record was added, I wanted to say "expire that page". Problem was that page contained ~1000 different keys.
|
51
|
+
So I needed something to store the keys for me and associate them with tags. That's exactly what cashier does.
|
52
|
+
Cache associate individual cache keys with a tag, then expire them all at once. This took my 7 layer loop
|
53
|
+
down to one line of code. It's also made managing the cache throught my application much easier.
|
54
|
+
|
55
|
+
## Why Tag Based Caching is Useful
|
56
|
+
|
57
|
+
1. You don't worry about keys. How many times have you created a complicated key for a fragment or action
|
58
|
+
then messed up when you tried to expire the cache
|
59
|
+
2. Associate your cached content into groups of related content. If you have records that are closely associated
|
60
|
+
or displayed together, then you can tag them and expire them at once.
|
61
|
+
3. **Expire cached content from anywhere.** If you've done any serious development, you know that Rails caching
|
62
|
+
does not work (easily) outside the scope of an HTTP request. If you have background jobs that manipulate data
|
63
|
+
or potentially invalidate cached data, you know how much of a pain it is to say `expire_fragment` in some random code.
|
64
|
+
4. Don't do anything differently! All you have to do is pass `:tag => 'something'` into `cache` (in the view) or `caches_action`
|
65
|
+
in the controller.
|
66
|
+
|
67
|
+
## How it Works
|
68
|
+
|
69
|
+
Cashier hooks into Rails' `store_fragment` method using `alias_method_chain` to run some code that captures the key
|
70
|
+
and tag then stores that in the rails cache.
|
71
|
+
|
72
|
+
### Adapters
|
73
|
+
|
74
|
+
Cashier has 2 adapters for the tags storing, `:cache_store` or `:redis_store`.
|
75
|
+
|
76
|
+
**IMPORTANT**: this store is ONLY for the tags, your fragments will still be stored in `Rails.cache`.
|
77
|
+
|
78
|
+
#### Setting an adapter for working with the cache as the tags storage
|
79
|
+
|
80
|
+
`config/initializers/cashier.rb`
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
Cachier.adapter = :cache_store
|
84
|
+
```
|
85
|
+
|
86
|
+
#### Setting an adapter for working with Redis as the tags storage
|
87
|
+
|
88
|
+
`config/initializers/cashier.rb`
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
Cashier.adapter = :redis_store
|
92
|
+
Cashier.adapter.redis = Redis.new(:host => '127.0.0.1', :port => '3697')
|
93
|
+
```
|
94
|
+
|
95
|
+
### Why Redis?
|
96
|
+
|
97
|
+
The reason Redis was introduced is that while the Rails.cache usage
|
98
|
+
for the tags store is clean and involves no "outer" dependencies,
|
99
|
+
since memcached is limited to read/write, it can slow down the application quite a bit.
|
100
|
+
|
101
|
+
If you work with very large arrays of keys and tags, you may see slowness in the cache communication.
|
102
|
+
|
103
|
+
Redis was introduces since it has the ability to work with "sets", and
|
104
|
+
you can add/remove tags from this set without reading the entire array.
|
105
|
+
|
106
|
+
|
107
|
+
### Benchmarking
|
108
|
+
|
109
|
+
Using the cache adapter, this piece of code takes 3 seconds on average
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
Benchmark.measure do
|
113
|
+
500.times do
|
114
|
+
key = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
|
115
|
+
tag = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
|
116
|
+
tag2 = (0...50).map{ ('a'..'z').to_a[rand(26)] }.join
|
117
|
+
Cashier.store_fragment(key, tag, tag2)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
```
|
121
|
+
|
122
|
+
Using the Redis adapter, the same piece of code takes 0.8 seconds, quite the difference :)
|
123
|
+
|
124
|
+
## Testing
|
125
|
+
|
126
|
+
Use can use cashier to test caching as well. First things first:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
# test.rb
|
130
|
+
|
131
|
+
config.application_controller.perform_caching = true
|
132
|
+
```
|
133
|
+
|
134
|
+
I've also included some Rspec Matchers and a cucumber helper for testing
|
135
|
+
caching. The rspec matchers can be used like this:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
describe "get index" do
|
139
|
+
include Cashier::Matchers
|
140
|
+
|
141
|
+
it "should cache the action" do
|
142
|
+
get :index
|
143
|
+
'some-tag'.should be_cached
|
144
|
+
end
|
145
|
+
end
|
146
|
+
```
|
147
|
+
|
148
|
+
Testing w/cucumber is more involved.
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# features/support/cashier.rb
|
152
|
+
require 'cashier/cucumber'
|
153
|
+
```
|
154
|
+
|
155
|
+
is an example of a possible step
|
156
|
+
|
157
|
+
```ruby
|
158
|
+
Then /the dashboard should be cached/ do
|
159
|
+
"dashboard".should be_cached
|
160
|
+
end
|
161
|
+
```
|
162
|
+
Including `cashier/cucumber` will also wipe the cache before every
|
163
|
+
scenario.
|
164
|
+
|
165
|
+
## Contributors
|
166
|
+
|
167
|
+
* [adman65](http://twitter.com/adman65) - Initial Implementation
|
168
|
+
* [KensoDev](http://twitter.com/kensodev) - Adding Redis support (Again \o/)
|
169
|
+
|
170
|
+
## Contributing to Cashier
|
171
|
+
|
172
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
|
173
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
|
174
|
+
* Fork the project
|
175
|
+
* Start a feature/bugfix branch
|
176
|
+
* Commit and push until you are happy with your contribution
|
177
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
178
|
+
* 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.
|
179
|
+
|
180
|
+
## Copyright
|
181
|
+
|
182
|
+
Copyright (c) 2010 Adam Hawkins. See LICENSE.txt for
|
183
|
+
further details.
|
data/Rakefile
CHANGED
@@ -3,13 +3,9 @@ Bundler::GemHelper.install_tasks
|
|
3
3
|
|
4
4
|
require 'rspec/core'
|
5
5
|
require 'rspec/core/rake_task'
|
6
|
+
|
6
7
|
RSpec::Core::RakeTask.new(:spec) do |spec|
|
7
8
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
8
9
|
end
|
9
10
|
|
10
|
-
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
11
|
-
spec.pattern = 'spec/**/*_spec.rb'
|
12
|
-
spec.rcov = true
|
13
|
-
end
|
14
|
-
|
15
11
|
task :default => :spec
|
data/cashier.gemspec
CHANGED
@@ -7,24 +7,21 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Cashier::VERSION
|
8
8
|
s.platform = Gem::Platform::RUBY
|
9
9
|
s.authors = ["Adam Hawkins"]
|
10
|
-
s.email = ["
|
11
|
-
s.homepage = "https://github.com/
|
12
|
-
s.summary = %q{Tag based caching for Rails}
|
10
|
+
s.email = ["me@broadcastingadam.com"]
|
11
|
+
s.homepage = "https://github.com/threadedlabs/cashier"
|
12
|
+
s.summary = %q{Tag based caching for Rails using Redis or Memcached}
|
13
13
|
s.description = %q{Associate different cached content with a tag, then expire by tag instead of key}
|
14
14
|
|
15
|
-
s.rubyforge_project = "cashier"
|
16
|
-
|
17
15
|
s.files = `git ls-files`.split("\n")
|
18
16
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
17
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
18
|
s.require_paths = ["lib"]
|
21
19
|
|
22
|
-
s.add_development_dependency 'rails'
|
20
|
+
s.add_development_dependency 'rails', '~> 3.0'
|
21
|
+
|
23
22
|
s.add_development_dependency 'rspec'
|
24
23
|
s.add_development_dependency 'rspec-rails'
|
25
|
-
s.add_development_dependency '
|
26
|
-
s.add_development_dependency '
|
27
|
-
s.add_development_dependency '
|
28
|
-
s.add_development_dependency 'sqlite3'
|
24
|
+
s.add_development_dependency 'dalli'
|
25
|
+
s.add_development_dependency 'simplecov'
|
26
|
+
s.add_development_dependency 'redis', '~> 2.2.0'
|
29
27
|
end
|
30
|
-
|
data/lib/cashier.rb
CHANGED
@@ -1,78 +1,136 @@
|
|
1
|
-
# Cashier
|
2
|
-
|
3
1
|
module Cashier
|
4
2
|
extend self
|
5
3
|
|
6
4
|
CACHE_KEY = 'cashier-tags'
|
7
5
|
|
6
|
+
def adapter
|
7
|
+
if @@adapter == :cache_store
|
8
|
+
Cashier::Adapters::CacheStore
|
9
|
+
else
|
10
|
+
Cashier::Adapters::RedisStore
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: set the adapter the Cashier module will use to store the keys
|
15
|
+
#
|
16
|
+
# cache_adapter - :cache_store / :redis_store
|
17
|
+
#
|
18
|
+
# Examples
|
19
|
+
#
|
20
|
+
# Cashier.adapter = :redis_store
|
21
|
+
#
|
22
|
+
def adapter=(cache_adapter)
|
23
|
+
@@adapter = cache_adapter
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: whether the module will perform caching or not. this is being set in the application layer .perform_caching configuration
|
27
|
+
#
|
28
|
+
# Examples
|
29
|
+
#
|
30
|
+
# Cashier.perform_caching?
|
31
|
+
# # => true
|
32
|
+
#
|
8
33
|
def perform_caching?
|
9
34
|
::ApplicationController.perform_caching
|
10
35
|
end
|
11
36
|
|
37
|
+
# Public: store a fragment with an array of tags for this fragment.
|
38
|
+
#
|
39
|
+
# fragment - cached fragment.
|
40
|
+
# tags - array of tags you want to assign this fragments.
|
41
|
+
#
|
42
|
+
# Examples
|
43
|
+
#
|
44
|
+
# Cachier.store_fragment('foo', 'tag1', 'tag2', 'tag3')
|
45
|
+
#
|
12
46
|
def store_fragment(fragment, *tags)
|
13
47
|
return unless perform_caching?
|
14
48
|
|
15
49
|
tags.each do |tag|
|
16
50
|
# store the fragment
|
17
|
-
|
18
|
-
Rails.cache.write(tag, fragments + [fragment])
|
51
|
+
adapter.store_fragment_in_tag(fragment, tag)
|
19
52
|
end
|
20
53
|
|
21
|
-
|
22
|
-
|
23
|
-
cashier_tags = (cashier_tags + tags).uniq
|
24
|
-
Rails.cache.write(CACHE_KEY, cashier_tags)
|
54
|
+
# now store the tag for book keeping
|
55
|
+
adapter.store_tags(tags)
|
25
56
|
end
|
26
57
|
|
58
|
+
# Public: expire tags. expiring the keys 'assigned' to the tags you expire and removes the tags from the tags list
|
59
|
+
#
|
60
|
+
# tags - array of tags to expire.
|
61
|
+
#
|
62
|
+
# Examples
|
63
|
+
#
|
64
|
+
# Cashier.expire('tag1', 'tag2')
|
65
|
+
#
|
27
66
|
def expire(*tags)
|
28
67
|
return unless perform_caching?
|
29
68
|
|
30
69
|
# delete them from the cache
|
31
70
|
tags.each do |tag|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
71
|
+
fragment_keys = adapter.get_fragments_for_tag(tag)
|
72
|
+
|
73
|
+
fragment_keys.each do |fragment_key|
|
74
|
+
Rails.cache.delete(fragment_key)
|
36
75
|
end
|
37
|
-
|
76
|
+
|
77
|
+
adapter.delete_tag(tag)
|
38
78
|
end
|
39
79
|
|
40
80
|
# now remove them from the list
|
41
81
|
# of stored tags
|
42
|
-
|
43
|
-
cashier_tags = (cashier_tags - tags).uniq
|
44
|
-
Rails.cache.write(CACHE_KEY, cashier_tags)
|
82
|
+
adapter.remove_tags(tags)
|
45
83
|
end
|
46
84
|
|
85
|
+
# Public: returns the array of tags stored in the tags store.
|
86
|
+
#
|
87
|
+
#
|
88
|
+
# Examples
|
89
|
+
#
|
90
|
+
# Cashier.tags
|
91
|
+
# # => ['tag1', 'tag2']
|
92
|
+
#
|
47
93
|
def tags
|
48
|
-
|
94
|
+
adapter.tags
|
49
95
|
end
|
50
96
|
|
97
|
+
# Public: clears the tags.
|
98
|
+
#
|
99
|
+
#
|
100
|
+
# Examples
|
101
|
+
#
|
102
|
+
# Cashier.clear
|
103
|
+
#
|
51
104
|
def clear
|
52
|
-
|
53
|
-
Rails.cache.delete(CACHE_KEY)
|
54
|
-
end
|
55
|
-
|
56
|
-
def wipe
|
57
|
-
clear
|
105
|
+
adapter.clear
|
58
106
|
end
|
59
107
|
|
108
|
+
# Public: get all the keys names as an array.
|
109
|
+
#
|
110
|
+
#
|
111
|
+
# Examples
|
112
|
+
#
|
113
|
+
# Cachier.keys
|
114
|
+
# # => ['key1', 'key2', 'key3']
|
115
|
+
#
|
60
116
|
def keys
|
61
|
-
|
62
|
-
arry += Rails.cache.fetch(tag)
|
63
|
-
end.compact
|
117
|
+
adapter.keys
|
64
118
|
end
|
65
119
|
|
120
|
+
# Public: get all the keys for a specific tag as an array.
|
121
|
+
#
|
122
|
+
#
|
123
|
+
# Examples
|
124
|
+
#
|
125
|
+
# Cashier.tags_for('tag1')
|
126
|
+
# # => ['key1', 'key2', 'key3']
|
127
|
+
#
|
66
128
|
def keys_for(tag)
|
67
|
-
|
129
|
+
adapter.get_fragments_for_tag(tag)
|
68
130
|
end
|
69
131
|
end
|
70
132
|
|
71
|
-
require '
|
72
|
-
require 'cashier/
|
73
|
-
|
74
|
-
|
75
|
-
if Rails::VERSION::MAJOR == 3
|
76
|
-
require 'cashier/railtie'
|
77
|
-
end
|
78
|
-
end
|
133
|
+
require 'rails'
|
134
|
+
require 'cashier/railtie'
|
135
|
+
require 'cashier/adapters/cache_store'
|
136
|
+
require 'cashier/adapters/redis_store'
|