cashier 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/.gitignore +3 -42
  2. data/README.md +183 -0
  3. data/Rakefile +1 -5
  4. data/cashier.gemspec +8 -11
  5. data/lib/cashier.rb +93 -35
  6. data/lib/cashier/adapters/cache_store.rb +45 -0
  7. data/lib/cashier/adapters/redis_store.rb +46 -0
  8. data/lib/cashier/application_controller.rb +28 -0
  9. data/lib/cashier/matchers.rb +0 -1
  10. data/lib/cashier/railtie.rb +4 -3
  11. data/lib/cashier/version.rb +1 -1
  12. data/spec/{application_controller_spec.rb → controllers/application_controller_spec.rb} +0 -8
  13. data/spec/{test_app → dummy}/.gitignore +0 -0
  14. data/spec/{test_app → dummy}/Gemfile +0 -0
  15. data/spec/{test_app → dummy}/README +0 -0
  16. data/spec/{test_app → dummy}/Rakefile +0 -0
  17. data/spec/{test_app → dummy}/app/controllers/application_controller.rb +0 -0
  18. data/spec/{test_app → dummy}/app/controllers/home_controller.rb +0 -0
  19. data/spec/{test_app → dummy}/app/helpers/application_helper.rb +0 -0
  20. data/spec/{test_app → dummy}/app/helpers/home_helper.rb +0 -0
  21. data/spec/{test_app → dummy}/app/views/home/index.html.erb +0 -0
  22. data/spec/{test_app → dummy}/app/views/layouts/application.html.erb +0 -0
  23. data/spec/{test_app → dummy}/config.ru +0 -0
  24. data/spec/{test_app → dummy}/config/application.rb +0 -4
  25. data/spec/{test_app → dummy}/config/boot.rb +0 -0
  26. data/spec/{test_app → dummy}/config/environment.rb +0 -0
  27. data/spec/{test_app → dummy}/config/environments/development.rb +0 -3
  28. data/spec/{test_app → dummy}/config/environments/production.rb +0 -3
  29. data/spec/{test_app → dummy}/config/environments/test.rb +1 -6
  30. data/spec/{test_app → dummy}/config/initializers/backtrace_silencers.rb +0 -0
  31. data/spec/{test_app → dummy}/config/initializers/inflections.rb +0 -0
  32. data/spec/{test_app → dummy}/config/initializers/mime_types.rb +0 -0
  33. data/spec/{test_app → dummy}/config/initializers/secret_token.rb +0 -0
  34. data/spec/{test_app → dummy}/config/initializers/session_store.rb +0 -0
  35. data/spec/{test_app → dummy}/config/locales/en.yml +0 -0
  36. data/spec/{test_app → dummy}/config/routes.rb +0 -0
  37. data/spec/{test_app → dummy}/db/seeds.rb +0 -0
  38. data/spec/{test_app → dummy}/lib/tasks/.gitkeep +0 -0
  39. data/spec/{test_app → dummy}/public/404.html +0 -0
  40. data/spec/{test_app → dummy}/public/422.html +0 -0
  41. data/spec/{test_app → dummy}/public/500.html +0 -0
  42. data/spec/{test_app → dummy}/public/favicon.ico +0 -0
  43. data/spec/{test_app → dummy}/public/images/rails.png +0 -0
  44. data/spec/{test_app → dummy}/public/javascripts/application.js +0 -0
  45. data/spec/{test_app → dummy}/public/javascripts/controls.js +0 -0
  46. data/spec/{test_app → dummy}/public/javascripts/dragdrop.js +0 -0
  47. data/spec/{test_app → dummy}/public/javascripts/effects.js +0 -0
  48. data/spec/{test_app → dummy}/public/javascripts/prototype.js +0 -0
  49. data/spec/{test_app → dummy}/public/javascripts/rails.js +0 -0
  50. data/spec/{test_app → dummy}/public/robots.txt +0 -0
  51. data/spec/{test_app → dummy}/public/stylesheets/.gitkeep +0 -0
  52. data/spec/{test_app → dummy}/script/rails +0 -0
  53. data/spec/{test_app → dummy}/test/performance/browsing_test.rb +0 -0
  54. data/spec/{test_app → dummy}/test/test_helper.rb +0 -0
  55. data/spec/{test_app → dummy}/vendor/plugins/.gitkeep +0 -0
  56. data/spec/lib/adapters/cache_store_spec.rb +75 -0
  57. data/spec/lib/adapters/redis_store_spec.rb +89 -0
  58. data/spec/lib/cashier_spec.rb +112 -0
  59. data/spec/spec_helper.rb +34 -8
  60. metadata +179 -214
  61. data/..gemspec +0 -21
  62. data/.infinity_test +0 -19
  63. data/.rvmrc +0 -1
  64. data/Gemfile.lock +0 -118
  65. data/lib/cashier/controller_helper.rb +0 -34
  66. data/readme.md +0 -120
  67. data/spec/cashier_spec.rb +0 -89
  68. data/spec/test_app/Gemfile.lock +0 -73
  69. data/spec/test_app/config/database.yml +0 -22
data/.gitignore CHANGED
@@ -1,42 +1,3 @@
1
- # rcov generated
2
- coverage
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 = ["adman1965@gmail.com"]
11
- s.homepage = "https://github.com/Adman65/cashier"
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 'infinity_test'
26
- s.add_development_dependency 'memcache-client'
27
- s.add_development_dependency 'ruby-debug19'
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
- fragments = Rails.cache.fetch(tag) || []
18
- Rails.cache.write(tag, fragments + [fragment])
51
+ adapter.store_fragment_in_tag(fragment, tag)
19
52
  end
20
53
 
21
- # now store the tag for book keeping
22
- cashier_tags = Rails.cache.fetch(CACHE_KEY) || []
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
- if fragment_keys = Rails.cache.fetch(tag)
33
- fragment_keys.each do |fragment_key|
34
- Rails.cache.delete(fragment_key)
35
- end
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
- Rails.cache.delete(tag)
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
- cashier_tags = Rails.cache.fetch(CACHE_KEY) || []
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
- Rails.cache.fetch(CACHE_KEY) || []
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
- expire(*tags)
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
- tags.inject([]) do |arry, tag|
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
- Rails.cache.fetch(tag) || []
129
+ adapter.get_fragments_for_tag(tag)
68
130
  end
69
131
  end
70
132
 
71
- require 'cashier/controller_helper'
72
- require 'cashier/matchers'
73
-
74
- if defined?(::Rails)
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'