cashier 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use 1.9.2@cashier
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+ # Add dependencies required to use your gem here.
3
+ # Example:
4
+ # gem "activesupport", ">= 2.3.5"
5
+
6
+ gem 'redis'
7
+ gem 'redis-namespace'
8
+
9
+ # Add dependencies to develop your gem here.
10
+ # Include everything needed to run rake, tests, features, etc.
11
+ group :development do
12
+ gem "rspec", "~> 2.3.0"
13
+ gem "bundler", "~> 1.0.0"
14
+ gem "jeweler", "~> 1.5.2"
15
+ gem "rcov", ">= 0"
16
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ GEM
2
+ remote: http://rubygems.org/
3
+ specs:
4
+ diff-lcs (1.1.2)
5
+ git (1.2.5)
6
+ jeweler (1.5.2)
7
+ bundler (~> 1.0.0)
8
+ git (>= 1.2.5)
9
+ rake
10
+ rake (0.8.7)
11
+ rcov (0.9.9)
12
+ redis (2.1.1)
13
+ redis-namespace (0.10.0)
14
+ redis (< 3.0.0)
15
+ rspec (2.3.0)
16
+ rspec-core (~> 2.3.0)
17
+ rspec-expectations (~> 2.3.0)
18
+ rspec-mocks (~> 2.3.0)
19
+ rspec-core (2.3.1)
20
+ rspec-expectations (2.3.0)
21
+ diff-lcs (~> 1.1.2)
22
+ rspec-mocks (2.3.0)
23
+
24
+ PLATFORMS
25
+ ruby
26
+
27
+ DEPENDENCIES
28
+ bundler (~> 1.0.0)
29
+ jeweler (~> 1.5.2)
30
+ rcov
31
+ redis
32
+ redis-namespace
33
+ rspec (~> 2.3.0)
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Adam Hawkins
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,51 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'rake'
11
+
12
+ require 'jeweler'
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "cashier"
16
+ gem.homepage = "http://github.com/Adman65/cashier"
17
+ gem.license = "MIT"
18
+ gem.summary = %Q{Tag based caching for Rails}
19
+ gem.description = %Q{Associate different cached content with a tag, then expire by tag instead of key}
20
+ gem.email = "Adman1965@gmail.com"
21
+ gem.authors = ["Adam Hawkins"]
22
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
23
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
24
+ # gem.add_runtime_dependency 'redis'
25
+ # gem.add_runtime_dependency 'redis-namespace'
26
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
27
+ end
28
+ Jeweler::RubygemsDotOrgTasks.new
29
+
30
+ require 'rspec/core'
31
+ require 'rspec/core/rake_task'
32
+ RSpec::Core::RakeTask.new(:spec) do |spec|
33
+ spec.pattern = FileList['spec/**/*_spec.rb']
34
+ end
35
+
36
+ RSpec::Core::RakeTask.new(:rcov) do |spec|
37
+ spec.pattern = 'spec/**/*_spec.rb'
38
+ spec.rcov = true
39
+ end
40
+
41
+ task :default => :spec
42
+
43
+ require 'rake/rdoctask'
44
+ Rake::RDocTask.new do |rdoc|
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+
47
+ rdoc.rdoc_dir = 'rdoc'
48
+ rdoc.title = "cashier #{version}"
49
+ rdoc.rdoc_files.include('README*')
50
+ rdoc.rdoc_files.include('lib/**/*.rb')
51
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/cashier.gemspec ADDED
@@ -0,0 +1,70 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{cashier}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Adam Hawkins"]
12
+ s.date = %q{2011-01-05}
13
+ s.description = %q{Associate different cached content with a tag, then expire by tag instead of key}
14
+ s.email = %q{Adman1965@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE.txt"
17
+ ]
18
+ s.files = [
19
+ ".rvmrc",
20
+ "Gemfile",
21
+ "Gemfile.lock",
22
+ "LICENSE.txt",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "cashier.gemspec",
26
+ "lib/cashier.rb",
27
+ "lib/cashier/controller_helper.rb",
28
+ "readme.md",
29
+ "spec/cashier_spec.rb",
30
+ "spec/spec_helper.rb"
31
+ ]
32
+ s.homepage = %q{http://github.com/Adman65/cashier}
33
+ s.licenses = ["MIT"]
34
+ s.require_paths = ["lib"]
35
+ s.rubygems_version = %q{1.3.7}
36
+ s.summary = %q{Tag based caching for Rails}
37
+ s.test_files = [
38
+ "spec/cashier_spec.rb",
39
+ "spec/spec_helper.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
47
+ s.add_runtime_dependency(%q<redis>, [">= 0"])
48
+ s.add_runtime_dependency(%q<redis-namespace>, [">= 0"])
49
+ s.add_development_dependency(%q<rspec>, ["~> 2.3.0"])
50
+ s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
51
+ s.add_development_dependency(%q<jeweler>, ["~> 1.5.2"])
52
+ s.add_development_dependency(%q<rcov>, [">= 0"])
53
+ else
54
+ s.add_dependency(%q<redis>, [">= 0"])
55
+ s.add_dependency(%q<redis-namespace>, [">= 0"])
56
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
57
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
58
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
59
+ s.add_dependency(%q<rcov>, [">= 0"])
60
+ end
61
+ else
62
+ s.add_dependency(%q<redis>, [">= 0"])
63
+ s.add_dependency(%q<redis-namespace>, [">= 0"])
64
+ s.add_dependency(%q<rspec>, ["~> 2.3.0"])
65
+ s.add_dependency(%q<bundler>, ["~> 1.0.0"])
66
+ s.add_dependency(%q<jeweler>, ["~> 1.5.2"])
67
+ s.add_dependency(%q<rcov>, [">= 0"])
68
+ end
69
+ end
70
+
data/lib/cashier.rb ADDED
@@ -0,0 +1,77 @@
1
+ # Cashier
2
+
3
+ module Cashier
4
+ extend self
5
+
6
+ # used to track all the tags Cashier is storing
7
+ STORAGE_KEY = 'cashier-tags'
8
+
9
+ def perform_caching?
10
+ ::ApplicationController.perform_caching
11
+ end
12
+
13
+ # shamefully taken straight from Resque.
14
+ # Thanks Defunkt :D
15
+
16
+ # Accepts:
17
+ # 1. A 'hostname:port' string
18
+ # 2. A 'hostname:port:db' string (to select the Redis db)
19
+ # 3. A 'hostname:port/namespace' string (to set the Redis namespace)
20
+ # 4. A redis URL string 'redis://host:port'
21
+ # 5. An instance of `Redis`, `Redis::Client`, `Redis::DistRedis`,
22
+ # or `Redis::Namespace`.
23
+ def redis=(server)
24
+ if server.respond_to? :split
25
+ if server =~ /redis\:\/\//
26
+ redis = Redis.connect(:url => server)
27
+ else
28
+ server, namespace = server.split('/', 2)
29
+ host, port, db = server.split(':')
30
+ redis = Redis.new(:host => host, :port => port,
31
+ :thread_safe => true, :db => db)
32
+ end
33
+ namespace ||= :cashier
34
+
35
+ @redis = Redis::Namespace.new(namespace, :redis => redis)
36
+ elsif server.respond_to? :namespace=
37
+ @redis = server
38
+ else
39
+ @redis = Redis::Namespace.new(:cashier, :redis => server)
40
+ end
41
+ end
42
+
43
+ # Returns the current Redis connection. If none has been created, will
44
+ # create a new one.
45
+ def redis
46
+ return @redis if @redis
47
+ self.redis = 'localhost:6379'
48
+ self.redis
49
+ end
50
+
51
+ def expire(*tags)
52
+ return unless perform_caching?
53
+
54
+ tags.each do |tag|
55
+ # check to see if the tag exsists
56
+ # some redis versions return nil or []
57
+ members = redis.smembers(tags)
58
+ if members.is_a?(Array)
59
+ members.each do |cache_key|
60
+ Rails.cache.delete(cache_key)
61
+ end
62
+ redis.del(tag)
63
+ redis.srem(STORAGE_KEY, tag)
64
+ end
65
+ end
66
+ end
67
+
68
+ def tags
69
+ redis.smembers STORAGE_KEY
70
+ end
71
+
72
+ def wipe
73
+ expire(*tags)
74
+ end
75
+ end
76
+
77
+ require 'cashier/controller_helper'
@@ -0,0 +1,38 @@
1
+ # Hooks into ApplicationController's write_fragment method.
2
+ # write_fragment is used for action and fragment caching.
3
+ # Create an alias method chain to call our customer method
4
+ # which stores the associated key with the tag in a
5
+ # Redis Set. Then we can expire all those keys from anywhere
6
+ # in the code using Rails.cache.delete
7
+ #
8
+ # I use alias_method_chain instead of calling 'super'
9
+ # because there is a very rare case where someone
10
+ # may have redfined 'write_fragment' in their own
11
+ # controllers. Using an alias method chain
12
+ # keeps those methods intact.
13
+
14
+ module Cashier
15
+ module ControllerHelper
16
+ def self.included(klass)
17
+ klass.class_eval do
18
+ def write_fragment_with_tagged_key(key, content, options = nil)
19
+ if options && options[:tag] && Cashier.perform_caching?
20
+ passed_tags = case options[:tag].class.to_s
21
+ when 'Proc', 'Lambda'
22
+ options[:tag].call(self)
23
+ else
24
+ options[:tag]
25
+ end
26
+ tags = passed_tags.is_a?(Array) ? passed_tags : [passed_tags]
27
+ tags.each do |tag|
28
+ Cashier.redis.sadd tag, fragment_cache_key(key)
29
+ Cashier.redis.sadd Cashier::STORAGE_KEY, tag
30
+ end
31
+ end
32
+ write_fragment_without_tagged_key(key, content, options)
33
+ end
34
+ alias_method_chain :write_fragment, :tagged_key
35
+ end
36
+ end
37
+ end
38
+ end
data/readme.md ADDED
@@ -0,0 +1,105 @@
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
+ # in your view
8
+ cache @some_record, :tag => 'some-component'
9
+
10
+ # in another view
11
+ cache @some_releated_record, :tag => 'some-component'
12
+
13
+ # can have multiple tags
14
+ cache @something, :tag => ['dashboard', 'settings'] # can expire from either tag
15
+
16
+ # in your sweeper
17
+ Cashier.expire 'some-component' # don't worry about keys! Much easier to sweep with confidence
18
+
19
+ # in your controller
20
+ caches_action :tag => 'complicated-action', :cache_path => proc { |c|
21
+ # huge complicated mess of parameters
22
+ c.params
23
+ }
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.wipe
41
+
42
+ ## How it Came About
43
+
44
+ I work on an application that involves all sorts of caching. I try to use action caching whenever I possible.
45
+ I had an index action that had maybe ~20 different combination of filters and sorting. If you want to use
46
+ action caching you have to create a **unique** key for every combination. This created a nice 6 nested loop
47
+ to expire the cache. Once you had pagination, then you have even more combinations of possible cache keys.
48
+ I needed a better solution. I wanted to expire things logically as a viewed them on the page. IE, if
49
+ a record was added, I wanted to say "expire that page". Problem was that page contained ~1000 different keys.
50
+ So I needed something to store the keys for me and associate them with tags. That's exactly what cashier does.
51
+ Cache associate individual cache keys with a tag, then expire them all at once. This took my 7 layer loop
52
+ down two one line of code. It's also made managing the cache throught my application much easier.
53
+
54
+ ## Why Tag Based Caching is Useful
55
+
56
+ 1. You don't worry about keys. How many times have you created a complicated key for a fragment or action
57
+ then messed up when you tried to expire the cache
58
+ 2. Associate your cached content into groups of related content. If you have records that are closely associated
59
+ or displayed together, then you can tag them and expire them at once.
60
+ 3. **Expire cached content from anywhere.** If you've done any serious development, you know that Rails caching
61
+ does not work (easily) outside the scope of an HTTP request. If you have background jobs that manipulate data
62
+ or potentially invalidate cached data, you know how much of a pain it is to say `expire_fragment` in some random code.
63
+ 4. Don't do anything differently! All you have to do is pass `:tag => 'something'` into `cache` (in the view) or `caches_action`
64
+ in the controller.
65
+
66
+ ## How it Works
67
+
68
+ Cashier hooks into Rails' `expire_fragment` method using `alias_method_chain` to run some code that captures the key
69
+ and tag then stores that as a set in redis. Then uses the set members to loop over keys to deleting using `Rails.cache.delete`
70
+
71
+ ## Configuration
72
+
73
+ Cashier needs Redis to function correctly. Create a yaml file. You may call it `config/cashier.yml`
74
+
75
+ development: localhost:6379
76
+ test: localhost:6379/test
77
+
78
+ Then write a simple initializer to configure Cahiser. Drop this file in in `config/initializers/cashier.rb`
79
+
80
+ resque_config = YAML.load_file(Raisl.root.join 'config', 'cashiser.yml')
81
+ Cashier.redis = resque_config[Rails.env]
82
+
83
+ Now in your `application_controller.rb` file just include the module
84
+
85
+ class ApplicationController < ActionController::Base
86
+ include Cashier::ControllerHelper
87
+ end
88
+
89
+ Now you're good to go!
90
+
91
+ ## Contributing to Cashier
92
+
93
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
94
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
95
+ * Fork the project
96
+ * Start a feature/bugfix branch
97
+ * Commit and push until you are happy with your contribution
98
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
99
+ * 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.
100
+
101
+ ## Copyright
102
+
103
+ Copyright (c) 2010 Adam Hawkins. See LICENSE.txt for
104
+ further details.
105
+
@@ -0,0 +1,7 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe "Cashier" do
4
+ it "fails" do
5
+ fail "hey buddy, you should probably rename this file and start specing for real"
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rspec'
4
+ require 'cashier'
5
+
6
+ # Requires supporting files with custom matchers and macros, etc,
7
+ # in ./support/ and its subdirectories.
8
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
9
+
10
+ RSpec.configure do |config|
11
+
12
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cashier
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Adam Hawkins
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-01-05 00:00:00 -08:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: redis
22
+ requirement: &id001 !ruby/object:Gem::Requirement
23
+ none: false
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :runtime
31
+ prerelease: false
32
+ version_requirements: *id001
33
+ - !ruby/object:Gem::Dependency
34
+ name: redis-namespace
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ none: false
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ segments:
41
+ - 0
42
+ version: "0"
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: *id002
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: &id003 !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ segments:
54
+ - 2
55
+ - 3
56
+ - 0
57
+ version: 2.3.0
58
+ type: :development
59
+ prerelease: false
60
+ version_requirements: *id003
61
+ - !ruby/object:Gem::Dependency
62
+ name: bundler
63
+ requirement: &id004 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ~>
67
+ - !ruby/object:Gem::Version
68
+ segments:
69
+ - 1
70
+ - 0
71
+ - 0
72
+ version: 1.0.0
73
+ type: :development
74
+ prerelease: false
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: jeweler
78
+ requirement: &id005 !ruby/object:Gem::Requirement
79
+ none: false
80
+ requirements:
81
+ - - ~>
82
+ - !ruby/object:Gem::Version
83
+ segments:
84
+ - 1
85
+ - 5
86
+ - 2
87
+ version: 1.5.2
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: rcov
93
+ requirement: &id006 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ type: :development
102
+ prerelease: false
103
+ version_requirements: *id006
104
+ description: Associate different cached content with a tag, then expire by tag instead of key
105
+ email: Adman1965@gmail.com
106
+ executables: []
107
+
108
+ extensions: []
109
+
110
+ extra_rdoc_files:
111
+ - LICENSE.txt
112
+ files:
113
+ - .rvmrc
114
+ - Gemfile
115
+ - Gemfile.lock
116
+ - LICENSE.txt
117
+ - Rakefile
118
+ - VERSION
119
+ - cashier.gemspec
120
+ - lib/cashier.rb
121
+ - lib/cashier/controller_helper.rb
122
+ - readme.md
123
+ - spec/cashier_spec.rb
124
+ - spec/spec_helper.rb
125
+ has_rdoc: true
126
+ homepage: http://github.com/Adman65/cashier
127
+ licenses:
128
+ - MIT
129
+ post_install_message:
130
+ rdoc_options: []
131
+
132
+ require_paths:
133
+ - lib
134
+ required_ruby_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ hash: -1411570206960376896
140
+ segments:
141
+ - 0
142
+ version: "0"
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ segments:
149
+ - 0
150
+ version: "0"
151
+ requirements: []
152
+
153
+ rubyforge_project:
154
+ rubygems_version: 1.3.7
155
+ signing_key:
156
+ specification_version: 3
157
+ summary: Tag based caching for Rails
158
+ test_files:
159
+ - spec/cashier_spec.rb
160
+ - spec/spec_helper.rb