super_cache 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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