super_cache 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - ree
5
+ - 1.9.2
6
+ - 1.9.3
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
- source 'https://rubygems.org'
1
+ source 'http://rubygems.org'
2
+ #source 'http://ruby.taobao.org'
2
3
 
3
4
  # Specify your gem's dependencies in super_cache.gemspec
4
5
  gemspec
6
+ gem 'rake'
7
+ gem 'rails', '~> 3.1'
8
+ #gem 'mocha', :require => false
data/README.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![Build Status](https://travis-ci.org/ShiningRay/super_cache.png?branch=master)](https://travis-ci.org/ShiningRay/super_cache)
2
+
1
3
  # SuperCache
2
4
 
3
5
  SuperCache for rails is a caching middleware inspired by the "SuperCache" plugin
@@ -25,7 +27,8 @@ Just call `super_caches_page` inside your controller with the action names to be
25
27
  class MyController < ApplicationController
26
28
  super_caches_page :index
27
29
  def index
28
- # action to be
30
+ # action to be cached
31
+ @expires_in = 1.hour
29
32
  edn
30
33
  end
31
34
  ```
@@ -33,6 +36,11 @@ end
33
36
  Super_cache will store the response body into `Rails.cache`. The next time requesting
34
37
  that action will get the same result.
35
38
 
39
+ There are several instance variables can be used for controlling the cache:
40
+
41
+ * `@cache_path` control the cache key, defaults to the request uri.
42
+ * `@expires_in` control the expiration time, now defaults to 600 sec.
43
+
36
44
  ## Contributing
37
45
 
38
46
  1. Fork it
data/Rakefile CHANGED
@@ -1 +1,24 @@
1
+ # encoding: UTF-8
2
+
1
3
  require "bundler/gem_tasks"
4
+ require 'rake/testtask'
5
+ require 'rdoc/task'
6
+
7
+ Rake::TestTask.new do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = true
11
+ end
12
+
13
+ desc "Run tests"
14
+ task :default => :test
15
+
16
+ desc 'Generate documentation for InheritedResources.'
17
+ Rake::RDocTask.new(:rdoc) do |rdoc|
18
+ rdoc.rdoc_dir = 'rdoc'
19
+ rdoc.title = 'InheritedResources'
20
+ rdoc.options << '--line-numbers' << '--inline-source'
21
+ rdoc.rdoc_files.include('README.rdoc')
22
+ rdoc.rdoc_files.include('MIT-LICENSE')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
@@ -0,0 +1,100 @@
1
+ module SuperCache
2
+ class DogPileFilter
3
+ attr_accessor :options
4
+ def initialize(options={})
5
+ self.options = options
6
+ end
7
+ # 1. check the expiration info of target key
8
+ # 2. if target key is expired, add a lock against target key
9
+ # 2.1 if not expired, return the cache
10
+ # 3.1 if lock successfully, then continue to action
11
+ # 3.2 if lock failed, then obtain target key
12
+ # 3.2.1 if target key is missing, then
13
+ # 3.2.1.1 wait for N sec to check if the cache is generated
14
+ # 3.2.1.1.1 if timeout, go to action
15
+ # 3.2.2 target key is not missing, return the cache
16
+ # 3.3 go to the action and obtain response body
17
+ # 3.4 store the response body to the target key and set the expiration with 2x longer
18
+ # 3.5 store the expireation info of target key
19
+ def filter(controller, action=nil, &block)
20
+ action ||= block
21
+ return action.call unless controller.perform_caching
22
+ options = self.options.dup
23
+
24
+ options[:controller] = controller
25
+ options[:action] = action || block
26
+ options[:cache_path] ||= weird_cache_path(options)
27
+ options[:flag_key] = "expires_at:#{options[:cache_path]}"
28
+ options[:expires_in] ||= 600
29
+ options[:content] = nil
30
+
31
+ if Rails.cache.read(options[:flag_key], :raw => true)
32
+ check_cache(options)
33
+ else
34
+ cache_expired(options)
35
+ end
36
+ end
37
+
38
+ protected
39
+ def write_cache(options)
40
+ response = options[:controller].response
41
+ return if response.status.to_i != 200
42
+ expires_in = options[:expires_in].to_i
43
+ Rails.cache.write(options[:flag_key], expires_in, :raw => true, :expires_in => expires_in)
44
+ Rails.cache.write(options[:cache_path], response.body, :raw => true, :expires_in => expires_in * 2)
45
+ end
46
+
47
+ def check_cache(options)
48
+ if options[:content] = Rails.cache.read(options[:cache_path], :raw => true) and options[:content].size > 0
49
+ cache_hit(options)
50
+ else
51
+ cache_miss(options)
52
+ end
53
+ end
54
+
55
+ def cache_hit(options)
56
+ controller = options[:controller]
57
+ request = controller.request
58
+ headers = controller.headers
59
+ content = options[:content]
60
+ Rails.logger.info "Hit #{options[:cache_path]}"
61
+ headers['Content-Length'] ||= content.size.to_s
62
+ headers['Content-Type'] ||= request.format.to_s.strip unless request.format == :all
63
+ controller.send :render, :text => content, :content_type => 'text/html'
64
+ end
65
+
66
+ #when the target cache is not established yet
67
+ def cache_miss(options)
68
+ Lock.synchronize(options[:cache_path]) do
69
+ options[:action].call
70
+ write_cache(options)
71
+ end
72
+ rescue Lock::MaxRetriesError
73
+ options[:action].call
74
+ write_cache(options)
75
+ end
76
+
77
+ # the cache is expired
78
+ def cache_expired(options)
79
+ if Lock.acquire_lock(options[:cache_path])
80
+ options[:action].call
81
+ write_cache(options)
82
+ else
83
+ # haven't acquired lock, return stale cache
84
+ check_cache(options)
85
+ end
86
+ end
87
+
88
+ def weird_cache_path(options)
89
+ controller = options[:controller]
90
+ request = controller.request
91
+ path = File.join request.host, request.path
92
+ q = request.query_string
93
+ request.format ||= :html
94
+ format = request.format.to_sym
95
+ path = "#{path}.#{format}" if format != :html and format != :all and controller.params[:format].blank?
96
+ path = "#{path}?#{q}" if !q.empty? && q =~ /=/
97
+ path
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,44 @@
1
+ module SuperCache
2
+ class Lock
3
+ class MaxRetriesError < RuntimeError; end
4
+
5
+ DEFAULT_RETRY = 5
6
+ DEFAULT_EXPIRY = 30
7
+
8
+ class << self
9
+ def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
10
+ if recursive_lock?(key)
11
+ yield
12
+ else
13
+ begin
14
+ retries.times do |count|
15
+ return yield if acquire_lock(key, lock_expiry)
16
+ raise MaxRetriesError if count == retries - 1
17
+ exponential_sleep(count) unless count == retries - 1
18
+ end
19
+ raise MaxRetriesError, "Couldn't acquire memcache lock for: #{key}"
20
+ ensure
21
+ release_lock(key)
22
+ end
23
+ end
24
+ end
25
+
26
+ def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY)
27
+ Rails.cache.write("lock/#{key}", Process.pid, :unless_exist => true, :expires_in => lock_expiry)
28
+ end
29
+
30
+ def release_lock(key)
31
+ Rails.cache.delete("lock/#{key}")
32
+ end
33
+
34
+ def exponential_sleep(count)
35
+ Benchmark::measure { sleep((2**count) / 5.0) }
36
+ end
37
+
38
+ private
39
+ def recursive_lock?(key)
40
+ Rails.cache.read("lock/#{key}") == Process.pid
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,48 @@
1
+ module SuperCache
2
+ class SimpleFilter
3
+ def initialize(options={})
4
+
5
+ end
6
+
7
+ def filter(controller)
8
+ return yield unless controller.perform_caching
9
+ @cache_path = controller.instance_variable_get('@caches_path') || weird_cache_path(controller)
10
+ request = controller.request
11
+ response = controller.response
12
+ headers = response.headers
13
+
14
+ if content = Rails.cache.read(@cache_path, :raw => true)
15
+ return yield if content.size <= 0
16
+ Rails.logger.debug "Hit #{@cache_path}"
17
+ headers['Content-Length'] ||= content.size.to_s
18
+ headers['Content-Type'] ||= request.format.to_s.strip unless request.format == :all
19
+ controller.send :render, :text => content, :content_type => 'text/html'
20
+ return false
21
+ else
22
+ yield
23
+ body = response.body.to_s
24
+ return if controller.instance_variable_get('@no_cache') || body.size == 0 || response.status.to_i != 200
25
+ @expires_in = controller.instance_variable_get('@expires_in') || 600
26
+ Rails.logger.debug("Write #{@cache_path}")
27
+ Rails.cache.write(@cache_path, body, :raw => true, :expires_in => @expires_in.to_i)
28
+ end
29
+ rescue ArgumentError => e
30
+ @no_cache = true
31
+ Rails.logger.info e.to_s
32
+ Rails.logger.debug {e.backtrace}
33
+ end
34
+
35
+ private
36
+ def weird_cache_path(controller)
37
+ controller.instance_eval do
38
+ path = File.join request.host, request.path
39
+ q = request.query_string
40
+ request.format ||= :html
41
+ format = request.format.to_sym
42
+ path = "#{path}.#{format}" if format != :html and format != :all and params[:format].blank?
43
+ path = "#{path}?#{q}" if !q.empty? && q =~ /=/
44
+ path
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,3 +1,3 @@
1
1
  module SuperCache
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/super_cache.rb CHANGED
@@ -4,7 +4,12 @@ require 'fileutils'
4
4
  # for static-caching the generated html pages
5
5
 
6
6
  module SuperCache
7
+ autoload :Lock, 'super_cache/lock'
8
+ autoload :DogPileFilter, 'super_cache/dog_pile_filter'
9
+ autoload :SimpleFilter, 'super_cache/simple_filter'
10
+
7
11
  def self.included(base)
12
+ base.class_attribute :cache_filter
8
13
  base.extend(ClassMethods)
9
14
  end
10
15
 
@@ -13,66 +18,21 @@ module SuperCache
13
18
  return unless perform_caching
14
19
  options = pages.extract_options!
15
20
  options[:only] = (Array(options[:only]) + pages).flatten
16
- before_filter :check_weird_cache, options
17
- after_filter :weird_cache, options
21
+ self.cache_filter = if options.delete(:lock)
22
+ DogPileFilter.new
23
+ else
24
+ SimpleFilter.new
25
+ end
26
+ around_filter self.cache_filter, options
18
27
  end
19
28
 
20
29
  def skip_super_caches_page(*pages)
30
+ return unless self.cache_filter
21
31
  options = pages.extract_options!
22
32
  options[:only] = (Array(options[:only]) + pages).flatten
23
- skip_before_filter :check_weird_cache, options
24
- skip_after_filter :weird_cache, options
25
- end
26
- end
27
-
28
- def check_weird_cache
29
- return unless perform_caching
30
- @cache_path ||= weird_cache_path
31
-
32
- if content = Rails.cache.read(@cache_path, :raw => true)
33
- return if content.size <= 1
34
- logger.info "Hit #{@cache_path}"
35
-
36
- headers['Content-Length'] ||= content.size.to_s
37
- headers['Content-Type'] ||= request.format.to_s.strip unless request.format == :all
38
- render :text => content, :content_type => 'text/html'
39
- return false
33
+ skip_around_filter self.cache_filter, options
40
34
  end
41
- rescue ArgumentError => e
42
- @no_cache = true
43
- logger.info e.to_s
44
- logger.debug {e.backtrace}
45
35
  end
46
-
47
- def weird_cache
48
- return if @no_cache
49
- return unless perform_caching
50
- return if request.format.to_sym == :mobile
51
- @cache_path ||= weird_cache_path
52
- @expires_in ||= 600
53
- return if response.body.size <= 1
54
- return if response.respond_to?(:status) and response.status.to_i != 200
55
- #benchmark "Super Cached page: #{@cache_path}" do
56
- # @cache_subject = Array(@cache_subject)
57
- # @cache_subject.compact.flatten.select{|s|s.respond_to?(:append_cached_key)}.each do |subject|
58
- # subject.append_cached_key @cache_path
59
- # end
60
- Rails.cache.write(@cache_path, response.body, :raw => true, :expires_in => @expires_in.to_i)
61
- #end
62
- end
63
-
64
- protected :check_weird_cache
65
- protected :weird_cache
66
- private
67
- def weird_cache_path
68
- path = File.join request.host, request.path
69
- q = request.query_string
70
- request.format ||= :html
71
- format = request.format.to_sym
72
- path = "#{path}.#{format}" if format != :html and format != :all and params[:format].blank?
73
- path = "#{path}?#{q}" #if !q.empty? && q =~ /=/
74
- path
75
- end
76
36
  end
77
37
 
78
38
  ActionController::Base.__send__ :include, SuperCache
data/super_cache.gemspec CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |gem|
9
9
  gem.authors = ["ShiningRay"]
10
10
  gem.email = ["tsowly@hotmail.com"]
11
11
  gem.description = %q{A simple caching middleware for rails}
12
- gem.summary = %q{A simple caching middleware for rails}
12
+ gem.summary = %q{A simple caching middleware for rails, with solution for dog-pile effect}
13
13
  gem.homepage = "https://github.com/shiningray/super_cache"
14
14
 
15
15
  gem.files = `git ls-files`.split($/)
@@ -0,0 +1,37 @@
1
+ require File.expand_path('test_helper', File.dirname(__FILE__))
2
+ require File.expand_path('my_controller', File.dirname(__FILE__))
3
+
4
+ class MyController < ApplicationController
5
+ around_filter do |controller, action |
6
+ DogPileFilter.new.filter(controller, action)
7
+ end
8
+ end
9
+
10
+ class DogPileFilterTest < ActionController::TestCase
11
+ tests MyController
12
+ setup do
13
+ MyController.counter = 0
14
+ end
15
+ test "should get index successfully and store cache and then get the cached version" do
16
+ get :index
17
+ assert_response :success
18
+ assert_equal '0', @response.body.strip
19
+ assert_equal '0', Rails.cache.read('test.host/my', :raw => true)
20
+ get :index
21
+ assert_response :success
22
+ assert_equal '0', @response.body.strip
23
+ end
24
+ test "should not cache when performing cache is disabled" do
25
+ ActionController::Base.perform_caching=false
26
+ get :index
27
+ assert_response :success
28
+ assert_equal '0', @response.body.strip
29
+ assert_nil Rails.cache.read('test.host/my', :raw => true)
30
+ ActionController::Base.perform_caching=true
31
+ end
32
+ test "should not cache when response is redirected" do
33
+ get :redirect
34
+ assert_response :redirect
35
+ assert_nil Rails.cache.read('test.host/redirect', :raw => true)
36
+ end
37
+ end
@@ -0,0 +1,2 @@
1
+ en:
2
+ flash:
@@ -0,0 +1,10 @@
1
+ class MyController < ApplicationController
2
+ cattr_accessor :counter
3
+ def index
4
+ render :text => counter
5
+ self.counter += 1
6
+ end
7
+ def redirect
8
+ redirect_to :action => :index
9
+ end
10
+ end
@@ -0,0 +1,38 @@
1
+ require File.expand_path('test_helper', File.dirname(__FILE__))
2
+ require File.expand_path('my_controller', File.dirname(__FILE__))
3
+ class MyController
4
+ super_caches_page :index
5
+ end
6
+ class SuperCacheTest < ActionController::TestCase
7
+ tests MyController
8
+ setup do
9
+ MyController.counter = 0
10
+ end
11
+ test "should get index successfully and store cache and then get the cached version" do
12
+ get :index
13
+ assert_response :success
14
+ assert_equal '0', @response.body.strip
15
+ assert_equal '0', Rails.cache.read('test.host/my', :raw => true)
16
+ get :index
17
+ assert_response :success
18
+ assert_equal '0', @response.body.strip
19
+ Rails.cache.delete('test.host/my')
20
+ get :index
21
+ assert_response :success
22
+ assert_equal '1', @response.body.strip
23
+ assert_equal '1', Rails.cache.read('test.host/my', :raw => true)
24
+ end
25
+ test "should not cache when performing cache is disabled" do
26
+ ActionController::Base.perform_caching=false
27
+ get :index
28
+ assert_response :success
29
+ assert_equal '0', @response.body.strip
30
+ assert_nil Rails.cache.read('test.host/my', :raw => true)
31
+ ActionController::Base.perform_caching=true
32
+ end
33
+ test "should not cache when response is redirected" do
34
+ get :redirect
35
+ assert_response :redirect
36
+ assert_nil Rails.cache.read('test.host/redirect', :raw => true)
37
+ end
38
+ end
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler.setup
5
+
6
+ require 'test/unit'
7
+ #require 'mocha/setup'
8
+
9
+ ENV["RAILS_ENV"] = "test"
10
+ RAILS_ROOT = "anywhere"
11
+
12
+ require "active_support"
13
+ require "active_model"
14
+ require "action_controller"
15
+
16
+ I18n.load_path << File.join(File.dirname(__FILE__), 'locales', 'en.yml')
17
+ I18n.reload!
18
+
19
+ class ApplicationController < ActionController::Base; end
20
+ class Rails
21
+ cattr_accessor :cache, :logger
22
+ @@cache = ActiveSupport::Cache::MemoryStore.new
23
+ @@logger = Logger.new($stderr)
24
+ end
25
+ # Add IR to load path and load the main file
26
+ $:.unshift File.expand_path(File.dirname(__FILE__) + '/../lib')
27
+ require 'super_cache'
28
+
29
+ ActionController::Base.view_paths = File.join(File.dirname(__FILE__), 'views')
30
+ ActionController::Base.perform_caching=true
31
+ SuperCache::Routes = ActionDispatch::Routing::RouteSet.new
32
+ SuperCache::Routes.draw do
33
+ match ':controller(/:action(/:id))'
34
+ match ':controller(/:action)'
35
+ resources 'posts'
36
+ root :to => 'posts#index'
37
+ end
38
+
39
+ ActionController::Base.send :include, SuperCache::Routes.url_helpers
40
+
41
+ class ActionController::TestCase
42
+ setup do
43
+ @routes = SuperCache::Routes
44
+ @request.host = 'test.host'
45
+ Rails.cache.clear
46
+ end
47
+ end
metadata CHANGED
@@ -1,53 +1,86 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: super_cache
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.1
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - ShiningRay
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2013-03-02 00:00:00.000000000 Z
17
+
18
+ date: 2013-03-08 00:00:00 Z
13
19
  dependencies: []
20
+
14
21
  description: A simple caching middleware for rails
15
- email:
22
+ email:
16
23
  - tsowly@hotmail.com
17
24
  executables: []
25
+
18
26
  extensions: []
27
+
19
28
  extra_rdoc_files: []
20
- files:
29
+
30
+ files:
21
31
  - .gitignore
32
+ - .travis.yml
22
33
  - Gemfile
23
34
  - LICENSE.txt
24
35
  - README.md
25
36
  - Rakefile
26
37
  - lib/super_cache.rb
38
+ - lib/super_cache/dog_pile_filter.rb
39
+ - lib/super_cache/lock.rb
40
+ - lib/super_cache/simple_filter.rb
27
41
  - lib/super_cache/version.rb
28
42
  - super_cache.gemspec
43
+ - test/dog_pile_filter_test.rb
44
+ - test/locales/en.yml
45
+ - test/my_controller.rb
46
+ - test/super_cache_test.rb
47
+ - test/test_helper.rb
29
48
  homepage: https://github.com/shiningray/super_cache
30
49
  licenses: []
50
+
31
51
  post_install_message:
32
52
  rdoc_options: []
33
- require_paths:
53
+
54
+ require_paths:
34
55
  - lib
35
- required_ruby_version: !ruby/object:Gem::Requirement
56
+ required_ruby_version: !ruby/object:Gem::Requirement
36
57
  none: false
37
- requirements:
38
- - - ! '>='
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ hash: 3
62
+ segments:
63
+ - 0
64
+ version: "0"
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
66
  none: false
43
- requirements:
44
- - - ! '>='
45
- - !ruby/object:Gem::Version
46
- version: '0'
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
47
74
  requirements: []
75
+
48
76
  rubyforge_project:
49
- rubygems_version: 1.8.24
77
+ rubygems_version: 1.8.16
50
78
  signing_key:
51
79
  specification_version: 3
52
- summary: A simple caching middleware for rails
53
- test_files: []
80
+ summary: A simple caching middleware for rails, with solution for dog-pile effect
81
+ test_files:
82
+ - test/dog_pile_filter_test.rb
83
+ - test/locales/en.yml
84
+ - test/my_controller.rb
85
+ - test/super_cache_test.rb
86
+ - test/test_helper.rb