├── CHANGELOG ├── LICENSE ├── README ├── Rakefile ├── defaults ├── extensions.rb.default ├── memcached.yml.default └── memcached_ctl.default ├── init.rb ├── install.rb ├── lib ├── acts_as_cached.rb └── acts_as_cached │ ├── benchmarking.rb │ ├── cache_methods.rb │ ├── config.rb │ ├── disabled.rb │ ├── fragment_cache.rb │ ├── local_cache.rb │ └── recipes.rb ├── tasks └── memcached.rake └── test ├── benchmarking_test.rb ├── cache_test.rb ├── config_test.rb ├── disabled_test.rb ├── extensions_test.rb ├── fragment_cache_test.rb ├── helper.rb ├── local_cache_test.rb └── sti_test.rb /CHANGELOG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/cache_fu/907cd55a53c51aa81ac174c40712193e77ac2e76/CHANGELOG -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == cache_fu 2 | 3 | A rewrite of acts_as_cached. 4 | 5 | == Changes from acts_as_cached 1 6 | 7 | - You can no longer set a 'ttl' method on a class. Instead, 8 | pass :ttl to acts_as_cached: 9 | >> acts_as_cached :ttl => 15.minutes 10 | 11 | - The is_cached? method is aliased as cached? 12 | 13 | - set_cache on an instance can take a ttl 14 | >> @story.set_cache(15.days) 15 | 16 | 17 | Chris Wanstrath [ chris[at]ozmm[dot]org ] 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | require 'load_multi_rails_rake_tasks' 5 | 6 | desc "Run all the tests" 7 | task :default => :test 8 | 9 | test_files = FileList['test/*test.rb'] 10 | 11 | desc 'Test the cache_fu plugin.' 12 | task :test do 13 | test_files.each do |file| 14 | ruby "#{file}" 15 | end 16 | end 17 | 18 | desc 'Test the cache_fu plugin against Rails 1.2.5' 19 | task :test_with_125 do 20 | ENV['MULTIRAILS_RAILS_VERSION'] = '1.2.5' 21 | test_files.each do |file| 22 | ruby "#{file}" 23 | end 24 | end 25 | 26 | desc "Run cache_fu tests using a memcache daemon" 27 | task :test_with_memcache do 28 | test_files.each do |file| 29 | ruby "#{file} with-memcache" 30 | end 31 | end 32 | 33 | desc 'Generate RDoc documentation for the cache_fu plugin.' 34 | Rake::RDocTask.new(:rdoc) do |rdoc| 35 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 36 | rdoc.rdoc_files.add(files) 37 | rdoc.main = "README" # page to start on 38 | rdoc.title = "cache_fu" 39 | rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 40 | rdoc.rdoc_dir = 'doc' # rdoc output folder 41 | rdoc.options << '--inline-source' 42 | end 43 | -------------------------------------------------------------------------------- /defaults/extensions.rb.default: -------------------------------------------------------------------------------- 1 | ## 2 | # Copy this file to vendor/plugins/acts_as_cached/extensions.rb if you 3 | # wish to extend acts_as_cached with your own instance or class methods. 4 | # 5 | # You can, of course, do this directly in your cached classes, 6 | # but keeping your custom methods here allows you to define 7 | # methods for all cached objects DRYly. 8 | module ActsAsCached 9 | module Extensions 10 | module ClassMethods 11 | ## 12 | # All acts_as_cached classes will be extended with 13 | # this method. 14 | # 15 | # >> Story.multi_get_cache(13, 353, 1231, 505) 16 | # => [, , ...] 17 | def multi_get_cache(*ids) 18 | ids.flatten.map { |id| get_cache(id) } 19 | end 20 | end 21 | 22 | module InstanceMethods 23 | ## 24 | # All instances of a acts_as_cached class will be 25 | # extended with this method. 26 | # 27 | # => story = Story.get_cache(1) 28 | # => 29 | # >> story.reset_included_caches 30 | # => true 31 | def reset_included_caches 32 | return false unless associations = cache_config[:include] 33 | associations.each do |association| 34 | Array(send(association)).each { |item| item.reset_cache } 35 | end 36 | true 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /defaults/memcached.yml.default: -------------------------------------------------------------------------------- 1 | defaults: 2 | ttl: 1800 3 | readonly: false 4 | urlencode: false 5 | c_threshold: 10000 6 | compression: true 7 | debug: false 8 | namespace: app 9 | sessions: false 10 | session_servers: false 11 | fragments: false 12 | memory: 64 13 | servers: localhost:11211 14 | benchmarking: true 15 | raise_errors: true 16 | fast_hash: false 17 | fastest_hash: false 18 | 19 | development: 20 | sessions: false 21 | fragments: false 22 | servers: localhost:11211 23 | 24 | # turn off caching 25 | test: 26 | disabled: true 27 | 28 | production: 29 | memory: 256 30 | benchmarking: false 31 | servers: 32 | - 192.185.254.121:11211 33 | - 192.185.254.138:11211 34 | - 192.185.254.160:11211 35 | -------------------------------------------------------------------------------- /defaults/memcached_ctl.default: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # By atmos@atmos.org 3 | # this goes in your script/ directory 4 | # it parses your memcached.yml file and hooks you up w/ some info 5 | # it keeps you from having to mess w/ stale memcached daemons for whatever reason. 6 | require 'yaml' 7 | require 'timeout' 8 | require 'erb' 9 | 10 | class MemcachedCtl 11 | attr_accessor :memcached, :memory, :pids, :servers, :ip_address, :ethernet_device 12 | 13 | def initialize 14 | env = ENV['RAILS_ENV'] || 'development' 15 | self.memcached = `which memcached`.chomp 16 | self.servers = [ ] 17 | self.pids = { } 18 | self.ethernet_device = ENV['ETH'] || 'eth0' 19 | self.ip_address = get_ip_address || '0.0.0.0' 20 | self.memory = '128' 21 | 22 | config = YAML.load(ERB.new(IO.read((File.expand_path(File.dirname(__FILE__) + "/../config/memcached.yml")))).result) 23 | self.servers = [ config['defaults']['servers'] ].flatten rescue ['127.0.0.1:11211'] 24 | self.servers = [ config[env]['servers'] ].flatten if config[env]['servers'] 25 | self.servers.reject! { |server| host,port = server.split(/:/); self.ip_address == host } 26 | self.memory = config[env]['memory'] unless config[env]['memory'].nil? 27 | 28 | each_server do |host,port| 29 | `ps auwwx | grep memcached | grep '\\-l #{ip_address} \\-p #{port}' | grep -v grep`.split(/\n/).each do |line| 30 | self.pids[port] = line.split(/\s+/)[1] 31 | end 32 | self.pids[port] ||= 'Down' 33 | end 34 | end 35 | 36 | def execute(cmd) 37 | send(cmd) rescue usage 38 | end 39 | 40 | def restart; stop; sleep 1; start end 41 | 42 | def status 43 | each_server { |host,port| puts "Port #{port} -> #{pids[port] =~ /\d+/ ? 'Up' : 'Down'}" } 44 | end 45 | 46 | def kill 47 | each_server { |host,port| `kill -9 #{pids[port]} > /dev/null 2>&1` if pids[port] =~ /\d+/ } 48 | end 49 | 50 | def stop; kill end 51 | 52 | def start 53 | each_server do |host,port| 54 | `#{memcached} -d -m #{memory} -l #{ip_address} -p #{port}` 55 | STDERR.puts "Try memcached_ctl status" unless $? == 0 56 | end 57 | end 58 | 59 | def usage 60 | methods = %w[start stop restart kill status] 61 | puts "Usage: script/memcached_ctl [ " + (methods * ' | ') + " ]" 62 | end 63 | 64 | protected 65 | def each_server 66 | servers.each do |server| 67 | host, port = server.split(/:/) 68 | yield host, port 69 | end 70 | end 71 | 72 | def get_ip_address # this works on linux you might have to tweak this on other oses 73 | line = `/sbin/ifconfig #{ethernet_device} | grep inet | grep -v inet6`.chomp 74 | if line =~ /\s*inet addr:((\d+\.){3}\d+)\s+.*/ 75 | self.ip_address = $1 76 | end 77 | end 78 | end 79 | ########################################################################### 80 | 81 | MemcachedCtl.new.execute(ARGV.first) 82 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'memcache' 3 | rescue LoadError 4 | end 5 | 6 | begin 7 | require 'memcached' 8 | rescue LoadError 9 | end 10 | 11 | begin 12 | require 'mem_cache_with_consistent_hashing' 13 | rescue LoadError 14 | end 15 | 16 | puts "=> You should be using the `memcache-client' gem. You're using RubyMemcache!" if Object.const_defined?(:RubyMemcache) 17 | 18 | require 'acts_as_cached' 19 | 20 | Object.send :include, ActsAsCached::Mixin 21 | 22 | unless File.exists? config_file = File.join(RAILS_ROOT, 'config', 'memcached.yml') 23 | error = "No config file found. Make sure you used `script/plugin install' and have memcached.yml in your config directory." 24 | puts error 25 | logger.error error 26 | exit! 27 | end 28 | 29 | ActsAsCached.config = YAML.load(ERB.new(IO.read(config_file)).result) 30 | 31 | begin 32 | require 'extensions' 33 | rescue LoadError 34 | end 35 | -------------------------------------------------------------------------------- /install.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Do some checks. 3 | puts 4 | 5 | $errors = 0 6 | 7 | puts "** Checking for memcached in path..." 8 | if `which memcached`.strip.empty? 9 | $errors += 1 10 | puts "!! Couldn't find memcached in your path. Are you sure you installed it? !!" 11 | puts "!! Check the README for help. You can't use acts_as_cached without it. !!" 12 | end 13 | 14 | puts "** Checking for memcache-client gem..." 15 | begin 16 | require 'rubygems' 17 | require 'memcache' 18 | rescue LoadError 19 | $errors += 1 20 | puts "!! Couldn't find memcache-client gem. You can't use acts_as_cached without it. !!" 21 | puts "!! $ sudo gem install memcache-client !!" 22 | end 23 | 24 | require 'fileutils' 25 | def copy_file(in_file, out_file) 26 | puts "** Trying to copy #{File.basename(in_file)} to #{out_file}..." 27 | begin 28 | if File.exists? out_file 29 | puts "!! You already have a #{out_file}. " + 30 | "Please check the default for new settings or format changes. !!" 31 | puts "!! You can find the default at #{in_file}. !!" 32 | $errors += 1 33 | else 34 | FileUtils.cp(in_file, out_file) 35 | end 36 | rescue 37 | $errors += 1 38 | puts "!! Error copying #{File.basename(in_file)} to #{out_file}. Please try by hand. !!" 39 | end 40 | end 41 | 42 | defaults_dir = File.join(File.dirname(__FILE__), 'defaults') 43 | 44 | config_yaml = File.join('.', 'config', 'memcached.yml') 45 | default_yaml = File.join(defaults_dir, 'memcached.yml.default') 46 | copy_file(default_yaml, config_yaml) 47 | 48 | memcached_ctl = File.join('.', 'script', 'memcached_ctl') 49 | default_ctl = File.join(defaults_dir, 'memcached_ctl.default') 50 | copy_file(default_ctl, memcached_ctl) 51 | 52 | puts 53 | print $errors.zero? ? "**" : "!!" 54 | print " acts_as_cached installed with #{$errors.zero? ? 'no' : $errors} errors." 55 | print " Please edit the memcached.yml file to your liking." 56 | puts $errors.zero? ? "" : " !!" 57 | puts "** Now would be a good time to check out the README. Enjoy your day." 58 | puts 59 | -------------------------------------------------------------------------------- /lib/acts_as_cached.rb: -------------------------------------------------------------------------------- 1 | require 'acts_as_cached/config' 2 | require 'acts_as_cached/cache_methods' 3 | require 'acts_as_cached/fragment_cache' 4 | require 'acts_as_cached/benchmarking' 5 | require 'acts_as_cached/disabled' 6 | require 'acts_as_cached/local_cache' 7 | 8 | module ActsAsCached 9 | @@config = {} 10 | mattr_reader :config 11 | 12 | def self.config=(options) 13 | @@config = Config.setup options 14 | end 15 | 16 | def self.skip_cache_gets=(boolean) 17 | ActsAsCached.config[:skip_gets] = boolean 18 | end 19 | 20 | module Mixin 21 | def acts_as_cached(options = {}) 22 | extend ClassMethods 23 | include InstanceMethods 24 | 25 | extend Extensions::ClassMethods if defined? Extensions::ClassMethods 26 | include Extensions::InstanceMethods if defined? Extensions::InstanceMethods 27 | 28 | options.symbolize_keys! 29 | 30 | options[:store] ||= ActsAsCached.config[:store] 31 | options[:ttl] ||= ActsAsCached.config[:ttl] 32 | 33 | # convert the find_by shorthand 34 | if find_by = options.delete(:find_by) 35 | options[:finder] = "find_by_#{find_by}".to_sym 36 | options[:cache_id] = find_by 37 | end 38 | 39 | cache_config.replace options.reject { |key,| not Config.valued_keys.include? key } 40 | cache_options.replace options.reject { |key,| Config.valued_keys.include? key } 41 | 42 | Disabled.add_to self and return if ActsAsCached.config[:disabled] 43 | Benchmarking.add_to self if ActsAsCached.config[:benchmarking] 44 | end 45 | end 46 | 47 | class CacheException < StandardError; end 48 | class NoCacheStore < CacheException; end 49 | class NoGetMulti < CacheException; end 50 | end 51 | -------------------------------------------------------------------------------- /lib/acts_as_cached/benchmarking.rb: -------------------------------------------------------------------------------- 1 | require 'benchmark' 2 | 3 | module ActsAsCached 4 | module Benchmarking #:nodoc: 5 | def self.cache_runtime 6 | @@cache_runtime ||= 0.0 7 | end 8 | 9 | def self.cache_reset_runtime 10 | @@cache_runtime = nil 11 | end 12 | 13 | def cache_benchmark(title, log_level = Logger::DEBUG, use_silence = true) 14 | return yield unless logger && logger.level == log_level 15 | result = nil 16 | 17 | seconds = Benchmark.realtime { 18 | result = use_silence ? ActionController::Base.silence { yield } : yield 19 | } 20 | 21 | @@cache_runtime ||= 0.0 22 | @@cache_runtime += seconds 23 | 24 | logger.add(log_level, "==> #{title} (#{'%.5f' % seconds})") 25 | result 26 | end 27 | 28 | def fetch_cache_with_benchmarking(*args) 29 | cache_benchmark "Got #{cache_key args.first} from cache." do 30 | fetch_cache_without_benchmarking(*args) 31 | end 32 | end 33 | 34 | def set_cache_with_benchmarking(*args) 35 | cache_benchmark "Set #{cache_key args.first} to cache." do 36 | set_cache_without_benchmarking(*args) 37 | end 38 | end 39 | 40 | def expire_cache_with_benchmarking(*args) 41 | cache_benchmark "Deleted #{cache_key args.first} from cache." do 42 | expire_cache_without_benchmarking(*args) 43 | end 44 | end 45 | 46 | def self.add_to(klass) 47 | return if klass.respond_to? :fetch_cache_with_benchmarking 48 | klass.extend self 49 | 50 | class << klass 51 | alias_method_chain :fetch_cache, :benchmarking 52 | alias_method_chain :set_cache, :benchmarking 53 | alias_method_chain :expire_cache, :benchmarking 54 | 55 | def logger; RAILS_DEFAULT_LOGGER end unless respond_to? :logger 56 | end 57 | end 58 | 59 | def self.inject_into_logs! 60 | if ActionController::Base.private_method_defined?(:rendering_runtime) 61 | # Rails < 2.2 62 | ActionController::Base.send :alias_method_chain, :rendering_runtime, :memcache 63 | elsif ActionController::Base.private_method_defined?(:view_runtime) 64 | # Rails >= 2.2 65 | ActionController::Base.send :alias_method_chain, :view_runtime, :memcache 66 | else 67 | raise "Unknown Rails Version?!" 68 | end 69 | end 70 | end 71 | end 72 | 73 | module ActionController 74 | class Base 75 | def rendering_runtime_with_memcache(runtime) #:nodoc: 76 | cache_runtime = ActsAsCached::Benchmarking.cache_runtime 77 | ActsAsCached::Benchmarking.cache_reset_runtime 78 | rendering_runtime_without_memcache(runtime) + (cache_runtime.nonzero? ? " | Memcache: #{"%.5f" % cache_runtime}" : '') 79 | end 80 | 81 | def view_runtime_with_memcache #:nodoc: 82 | cache_runtime = ActsAsCached::Benchmarking.cache_runtime 83 | ActsAsCached::Benchmarking.cache_reset_runtime 84 | view_runtime_without_memcache + (cache_runtime.nonzero? ? ", Memcache: #{"%.0f" % (cache_runtime * 1000)}" : '') 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/acts_as_cached/cache_methods.rb: -------------------------------------------------------------------------------- 1 | module ActsAsCached 2 | module ClassMethods 3 | @@nil_sentinel = :_nil 4 | 5 | def cache_config 6 | config = ActsAsCached::Config.class_config[cache_name] ||= {} 7 | if name == cache_name 8 | config 9 | else 10 | # sti 11 | ActsAsCached::Config.class_config[name] ||= config.dup 12 | end 13 | end 14 | 15 | def cache_options 16 | cache_config[:options] ||= {} 17 | end 18 | 19 | def get_cache(*args) 20 | options = args.last.is_a?(Hash) ? args.pop : {} 21 | args = args.flatten 22 | 23 | ## 24 | # head off to get_caches if we were passed multiple cache_ids 25 | if args.size > 1 26 | return get_caches(args, options) 27 | else 28 | cache_id = args.first 29 | end 30 | 31 | if (item = fetch_cache(cache_id)).nil? 32 | set_cache(cache_id, block_given? ? yield : fetch_cachable_data(cache_id), options[:ttl]) 33 | else 34 | @@nil_sentinel == item ? nil : item 35 | end 36 | end 37 | 38 | ## 39 | # This method accepts an array of cache_ids which it will use to call 40 | # get_multi on your cache store. Any misses will be fetched and saved to 41 | # the cache, and a hash keyed by cache_id will ultimately be returned. 42 | # 43 | # If your cache store does not support #get_multi an exception will be raised. 44 | def get_caches(*args) 45 | raise NoGetMulti unless cache_store.respond_to? :get_multi 46 | 47 | options = args.last.is_a?(Hash) ? args.pop : {} 48 | cache_ids = args.flatten.map(&:to_s) 49 | keys = cache_keys(cache_ids) 50 | 51 | # Map memcache keys to object cache_ids in { memcache_key => object_id } format 52 | keys_map = Hash[*keys.zip(cache_ids).flatten] 53 | 54 | # Call get_multi and figure out which keys were missed based on what was a hit 55 | hits = ActsAsCached.config[:disabled] ? {} : (cache_store(:get_multi, *keys) || {}) 56 | 57 | # Misses can take the form of key => nil 58 | hits.delete_if { |key, value| value.nil? } 59 | 60 | misses = keys - hits.keys 61 | hits.each { |k, v| hits[k] = nil if v == @@nil_sentinel } 62 | 63 | # Return our hash if there are no misses 64 | return hits.values.index_by(&:cache_id) if misses.empty? 65 | 66 | # Find any missed records 67 | needed_ids = keys_map.values_at(*misses) 68 | missed_records = Array(fetch_cachable_data(needed_ids)) 69 | 70 | # Cache the missed records 71 | missed_records.each { |missed_record| missed_record.set_cache(options[:ttl]) } 72 | 73 | # Return all records as a hash indexed by object cache_id 74 | (hits.values + missed_records).index_by(&:cache_id) 75 | end 76 | 77 | # simple wrapper for get_caches that 78 | # returns the items as an ordered array 79 | def get_caches_as_list(*args) 80 | cache_ids = args.last.is_a?(Hash) ? args.first : args 81 | cache_ids = [cache_ids].flatten 82 | hash = get_caches(*args) 83 | 84 | cache_ids.map do |key| 85 | hash[key] 86 | end 87 | end 88 | 89 | def set_cache(cache_id, value, ttl = nil) 90 | returning(value) do |v| 91 | v = @@nil_sentinel if v.nil? 92 | cache_store(:set, cache_key(cache_id), v, ttl || cache_config[:ttl] || 1500) 93 | end 94 | end 95 | 96 | def expire_cache(cache_id = nil) 97 | cache_store(:delete, cache_key(cache_id)) 98 | true 99 | end 100 | alias :clear_cache :expire_cache 101 | 102 | def reset_cache(cache_id = nil) 103 | set_cache(cache_id, fetch_cachable_data(cache_id)) 104 | end 105 | 106 | ## 107 | # Encapsulates the pattern of writing custom cache methods 108 | # which do nothing but wrap custom finders. 109 | # 110 | # => Story.caches(:find_popular) 111 | # 112 | # is the same as 113 | # 114 | # def self.cached_find_popular 115 | # get_cache(:find_popular) { find_popular } 116 | # end 117 | # 118 | # The method also accepts both a :ttl and/or a :with key. 119 | # Obviously the :ttl value controls how long this method will 120 | # stay cached, while the :with key's value will be passed along 121 | # to the method. The hash of the :with key will be stored with the key, 122 | # making two near-identical #caches calls with different :with values utilize 123 | # different caches. 124 | # 125 | # => Story.caches(:find_popular, :with => :today) 126 | # 127 | # is the same as 128 | # 129 | # def self.cached_find_popular 130 | # get_cache("find_popular:today") { find_popular(:today) } 131 | # end 132 | # 133 | # If your target method accepts multiple parameters, pass :withs an array. 134 | # 135 | # => Story.caches(:find_popular, :withs => [ :one, :two ]) 136 | # 137 | # is the same as 138 | # 139 | # def self.cached_find_popular 140 | # get_cache("find_popular:onetwo") { find_popular(:one, :two) } 141 | # end 142 | def caches(method, options = {}) 143 | if options.keys.include?(:with) 144 | with = options.delete(:with) 145 | get_cache("#{method}:#{with}", options) { send(method, with) } 146 | elsif withs = options.delete(:withs) 147 | get_cache("#{method}:#{withs}", options) { send(method, *withs) } 148 | else 149 | get_cache(method, options) { send(method) } 150 | end 151 | end 152 | alias :cached :caches 153 | 154 | def cached?(cache_id = nil) 155 | fetch_cache(cache_id).nil? ? false : true 156 | end 157 | alias :is_cached? :cached? 158 | 159 | def fetch_cache(cache_id) 160 | return if ActsAsCached.config[:skip_gets] 161 | 162 | autoload_missing_constants do 163 | cache_store(:get, cache_key(cache_id)) 164 | end 165 | end 166 | 167 | def fetch_cachable_data(cache_id = nil) 168 | finder = cache_config[:finder] || :find 169 | return send(finder) unless cache_id 170 | 171 | args = [cache_id] 172 | args << cache_options.dup unless cache_options.blank? 173 | send(finder, *args) 174 | end 175 | 176 | def cache_namespace 177 | cache_store(:namespace) 178 | end 179 | 180 | # Memcache-client automatically prepends the namespace, plus a colon, onto keys, so we take that into account for the max key length. 181 | # Rob Sanheim 182 | def max_key_length 183 | unless @max_key_length 184 | key_size = cache_config[:key_size] || 250 185 | @max_key_length = cache_namespace ? (key_size - cache_namespace.length - 1) : key_size 186 | end 187 | @max_key_length 188 | end 189 | 190 | def cache_name 191 | @cache_name ||= respond_to?(:base_class) ? base_class.name : name 192 | end 193 | 194 | def cache_keys(*cache_ids) 195 | cache_ids.flatten.map { |cache_id| cache_key(cache_id) } 196 | end 197 | 198 | def cache_key(cache_id) 199 | [cache_name, cache_config[:version], cache_id].compact.join(':').gsub(' ', '_')[0..(max_key_length - 1)] 200 | end 201 | 202 | def cache_store(method = nil, *args) 203 | return cache_config[:store] unless method 204 | 205 | load_constants = %w( get get_multi ).include? method.to_s 206 | 207 | swallow_or_raise_cache_errors(load_constants) do 208 | cache_config[:store].send(method, *args) 209 | end 210 | end 211 | 212 | def swallow_or_raise_cache_errors(load_constants = false, &block) 213 | load_constants ? autoload_missing_constants(&block) : yield 214 | rescue TypeError => error 215 | if error.to_s.include? 'Proc' 216 | raise MarshalError, "Most likely an association callback defined with a Proc is triggered, see http://ar.rubyonrails.com/classes/ActiveRecord/Associations/ClassMethods.html (Association Callbacks) for details on converting this to a method based callback" 217 | else 218 | raise error 219 | end 220 | rescue Exception => error 221 | if ActsAsCached.config[:raise_errors] 222 | raise error 223 | else 224 | RAILS_DEFAULT_LOGGER.debug "MemCache Error: #{error.message}" rescue nil 225 | nil 226 | end 227 | end 228 | 229 | def autoload_missing_constants 230 | yield 231 | rescue ArgumentError, MemCache::MemCacheError => error 232 | lazy_load ||= Hash.new { |hash, hash_key| hash[hash_key] = true; false } 233 | if error.to_s[/undefined class|referred/] && !lazy_load[error.to_s.split.last.sub(/::$/, '').constantize] then retry 234 | else raise error end 235 | end 236 | end 237 | 238 | module InstanceMethods 239 | def self.included(base) 240 | base.send :delegate, :cache_config, :to => 'self.class' 241 | base.send :delegate, :cache_options, :to => 'self.class' 242 | end 243 | 244 | def get_cache(key = nil, options = {}, &block) 245 | self.class.get_cache(cache_id(key), options, &block) 246 | end 247 | 248 | def set_cache(ttl = nil) 249 | self.class.set_cache(cache_id, self, ttl) 250 | end 251 | 252 | def reset_cache(key = nil) 253 | self.class.reset_cache(cache_id(key)) 254 | end 255 | 256 | def expire_cache(key = nil) 257 | self.class.expire_cache(cache_id(key)) 258 | end 259 | alias :clear_cache :expire_cache 260 | 261 | def cached?(key = nil) 262 | self.class.cached? cache_id(key) 263 | end 264 | 265 | def cache_key 266 | self.class.cache_key(cache_id) 267 | end 268 | 269 | def cache_id(key = nil) 270 | id = send(cache_config[:cache_id] || :id) 271 | key.nil? ? id : "#{id}:#{key}" 272 | end 273 | 274 | def caches(method, options = {}) 275 | key = "#{id}:#{method}" 276 | if options.keys.include?(:with) 277 | with = options.delete(:with) 278 | self.class.get_cache("#{key}:#{with}", options) { send(method, with) } 279 | elsif withs = options.delete(:withs) 280 | self.class.get_cache("#{key}:#{withs}", options) { send(method, *withs) } 281 | else 282 | self.class.get_cache(key, options) { send(method) } 283 | end 284 | end 285 | alias :cached :caches 286 | 287 | # Ryan King 288 | def set_cache_with_associations 289 | Array(cache_options[:include]).each do |assoc| 290 | send(assoc).reload 291 | end if cache_options[:include] 292 | set_cache 293 | end 294 | 295 | # Lourens Naud 296 | def expire_cache_with_associations(*associations_to_sweep) 297 | (Array(cache_options[:include]) + associations_to_sweep).flatten.uniq.compact.each do |assoc| 298 | Array(send(assoc)).compact.each { |item| item.expire_cache if item.respond_to?(:expire_cache) } 299 | end 300 | expire_cache 301 | end 302 | end 303 | 304 | class MarshalError < StandardError; end 305 | end 306 | -------------------------------------------------------------------------------- /lib/acts_as_cached/config.rb: -------------------------------------------------------------------------------- 1 | module ActsAsCached 2 | module Config 3 | extend self 4 | 5 | @@class_config = {} 6 | mattr_reader :class_config 7 | 8 | def valued_keys 9 | [ :store, :version, :pages, :per_page, :ttl, :finder, :cache_id, :find_by, :key_size ] 10 | end 11 | 12 | def setup(options) 13 | config = options['defaults'] 14 | 15 | case options[RAILS_ENV] 16 | when Hash then config.update(options[RAILS_ENV]) 17 | when String then config[:disabled] = true 18 | end 19 | 20 | config.symbolize_keys! 21 | 22 | setup_benchmarking! if config[:benchmarking] && !config[:disabled] 23 | 24 | setup_cache_store! config 25 | config 26 | end 27 | 28 | def setup_benchmarking! 29 | Benchmarking.inject_into_logs! 30 | end 31 | 32 | def setup_cache_store!(config) 33 | config[:store] = 34 | if config[:store].nil? 35 | setup_memcache config 36 | elsif config[:store].respond_to? :constantize 37 | config[:store].constantize.new 38 | else 39 | config[:store] 40 | end 41 | end 42 | 43 | def setup_memcache(config) 44 | config[:namespace] << "-#{RAILS_ENV}" 45 | 46 | # if someone (e.g., interlock) already set up memcached, then 47 | # we need to stop here 48 | return CACHE if Object.const_defined?(:CACHE) 49 | 50 | silence_warnings do 51 | Object.const_set :CACHE, memcache_client(config) 52 | Object.const_set :SESSION_CACHE, memcache_client(config) if config[:session_servers] 53 | end 54 | 55 | CACHE.servers = Array(config.delete(:servers)) 56 | SESSION_CACHE.servers = Array(config[:session_servers]) if config[:session_servers] 57 | 58 | setup_session_store if config[:sessions] 59 | setup_fragment_store! if config[:fragments] 60 | setup_fast_hash! if config[:fast_hash] 61 | setup_fastest_hash! if config[:fastest_hash] 62 | 63 | CACHE 64 | end 65 | 66 | def memcache_client(config) 67 | (config[:client] || "MemCache").classify.constantize.new(config) 68 | end 69 | 70 | def setup_session_store 71 | ActionController::Base.session_store = :mem_cache_store 72 | ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.update 'cache' => defined?(SESSION_CACHE) ? SESSION_CACHE : CACHE 73 | end 74 | 75 | def setup_fragment_store! 76 | ActsAsCached::FragmentCache.setup! 77 | end 78 | 79 | # break compatiblity with non-ruby memcache clients in exchange for speedup. 80 | # consistent across all platforms. 81 | def setup_fast_hash! 82 | def CACHE.hash_for(key) 83 | (0...key.length).inject(0) do |sum, i| 84 | sum + key[i] 85 | end 86 | end 87 | end 88 | 89 | # break compatiblity with non-ruby memcache clients in exchange for speedup. 90 | # NOT consistent across all platforms. Object#hash gives different results 91 | # on different architectures. only use if all your apps are running the 92 | # same arch. 93 | def setup_fastest_hash! 94 | def CACHE.hash_for(key) key.hash end 95 | end 96 | end 97 | end 98 | -------------------------------------------------------------------------------- /lib/acts_as_cached/disabled.rb: -------------------------------------------------------------------------------- 1 | module ActsAsCached 2 | module Disabled 3 | def fetch_cache_with_disabled(*args) 4 | nil 5 | end 6 | 7 | def set_cache_with_disabled(*args) 8 | args[1] 9 | end 10 | 11 | def expire_cache_with_disabled(*args) 12 | true 13 | end 14 | 15 | def self.add_to(klass) 16 | return if klass.respond_to? :fetch_cache_with_disabled 17 | klass.extend self 18 | 19 | class << klass 20 | alias_method_chain :fetch_cache, :disabled 21 | alias_method_chain :set_cache, :disabled 22 | alias_method_chain :expire_cache, :disabled 23 | end 24 | 25 | class << CACHE 26 | include FragmentCache::DisabledExtensions 27 | end if ActsAsCached.config[:fragments] && defined?(FragmentCache::DisabledExtensions) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/acts_as_cached/fragment_cache.rb: -------------------------------------------------------------------------------- 1 | module ActsAsCached 2 | module FragmentCache 3 | def self.setup! 4 | class << CACHE 5 | include Extensions 6 | end 7 | 8 | setup_fragment_cache_cache 9 | setup_rails_for_memcache_fragments 10 | setup_rails_for_action_cache_options 11 | end 12 | 13 | # add :ttl option to cache helper and set cache store memcache object 14 | def self.setup_rails_for_memcache_fragments 15 | if ::ActionView.const_defined?(:Template) 16 | # Rails 2.1+ 17 | ::ActionController::Base.cache_store = CACHE 18 | else 19 | # Rails < svn r8619 20 | ::ActionView::Helpers::CacheHelper.class_eval do 21 | def cache(name = {}, options = nil, &block) 22 | @controller.cache_erb_fragment(block, name, options) 23 | end 24 | end 25 | ::ActionController::Base.fragment_cache_store = CACHE 26 | end 27 | end 28 | 29 | def self.setup_fragment_cache_cache 30 | Object.const_set(:FragmentCacheCache, Class.new { acts_as_cached :store => CACHE }) 31 | end 32 | 33 | # add :ttl option to caches_action on the per action level by passing in a hash instead of an array 34 | # 35 | # Examples: 36 | # caches_action :index # will use the default ttl from your memcache.yml, or 25 minutes 37 | # caches_action :index => { :ttl => 5.minutes } # cache index action with 5 minute ttl 38 | # caches_action :page, :feed, :index => { :ttl => 2.hours } # cache index action with 2 hours ttl, all others use default 39 | # 40 | def self.setup_rails_for_action_cache_options 41 | ::ActionController::Caching::Actions::ActionCacheFilter.class_eval do 42 | # convert all actions into a hash keyed by action named, with a value of a ttl hash (to match other cache APIs) 43 | def initialize(*actions, &block) 44 | if [].respond_to?(:extract_options!) 45 | #edge 46 | @options = actions.extract_options! 47 | @actions = actions.inject(@options.except(:cache_path)) do |hsh, action| 48 | action.is_a?(Hash) ? hsh.merge(action) : hsh.merge(action => { :ttl => nil }) 49 | end 50 | @options.slice!(:cache_path) 51 | else 52 | #1.2.5 53 | @actions = actions.inject({}) do |hsh, action| 54 | action.is_a?(Hash) ? hsh.merge(action) : hsh.merge(action => { :ttl => nil }) 55 | end 56 | end 57 | end 58 | 59 | # override to skip caching/rendering on evaluated if option 60 | def before(controller) 61 | return unless @actions.include?(controller.action_name.intern) 62 | 63 | # maintaining edge and 1.2.x compatibility with this branch 64 | if @options 65 | action_cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller, path_options_for(controller, @options)) 66 | else 67 | action_cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller) 68 | end 69 | 70 | # should probably be like ActiveRecord::Validations.evaluate_condition. color me lazy. 71 | if conditional = @actions[controller.action_name.intern][:if] 72 | conditional = conditional.respond_to?(:call) ? conditional.call(controller) : controller.send(conditional) 73 | end 74 | @actions.delete(controller.action_name.intern) if conditional == false 75 | 76 | cache = controller.read_fragment(action_cache_path.path) 77 | if cache && (conditional || conditional.nil?) 78 | controller.rendered_action_cache = true 79 | if method(:set_content_type!).arity == 2 80 | set_content_type!(controller, action_cache_path.extension) 81 | else 82 | set_content_type!(action_cache_path) 83 | end 84 | controller.send(:render, :text => cache) 85 | false 86 | else 87 | # 1.2.x compatibility 88 | controller.action_cache_path = action_cache_path if controller.respond_to? :action_cache_path 89 | end 90 | end 91 | 92 | # override to pass along the ttl hash 93 | def after(controller) 94 | return if !@actions.include?(controller.action_name.intern) || controller.rendered_action_cache 95 | # 1.2.x compatibility 96 | path = controller.respond_to?(:action_cache_path) ? controller.action_cache_path.path : ActionController::Caching::Actions::ActionCachePath.path_for(controller) 97 | controller.write_fragment(path, controller.response.body, action_ttl(controller)) 98 | end 99 | 100 | private 101 | def action_ttl(controller) 102 | @actions[controller.action_name.intern] 103 | end 104 | end 105 | end 106 | 107 | module Extensions 108 | def read(*args) 109 | return if ActsAsCached.config[:skip_gets] 110 | FragmentCacheCache.cache_store(:get, args.first) 111 | end 112 | 113 | def write(name, content, options = {}) 114 | ttl = (options.is_a?(Hash) ? options[:ttl] : nil) || ActsAsCached.config[:ttl] || 25.minutes 115 | FragmentCacheCache.cache_store(:set, name, content, ttl) 116 | end 117 | end 118 | 119 | module DisabledExtensions 120 | def read(*args) nil end 121 | def write(*args) "" end 122 | end 123 | end 124 | end 125 | -------------------------------------------------------------------------------- /lib/acts_as_cached/local_cache.rb: -------------------------------------------------------------------------------- 1 | module ActsAsCached 2 | module LocalCache 3 | @@local_cache = {} 4 | mattr_accessor :local_cache 5 | 6 | def fetch_cache_with_local_cache(*args) 7 | @@local_cache[cache_key(args.first)] ||= fetch_cache_without_local_cache(*args) 8 | end 9 | 10 | def set_cache_with_local_cache(*args) 11 | @@local_cache[cache_key(args.first)] = set_cache_without_local_cache(*args) 12 | end 13 | 14 | def expire_cache_with_local_cache(*args) 15 | @@local_cache.delete(cache_key(args.first)) 16 | expire_cache_without_local_cache(*args) 17 | end 18 | alias :clear_cache_with_local_cache :expire_cache_with_local_cache 19 | 20 | def cached_with_local_cache?(*args) 21 | !!@@local_cache[cache_key(args.first)] || cached_without_local_cache?(*args) 22 | end 23 | 24 | def self.add_to(klass) 25 | return if klass.ancestors.include? self 26 | klass.send :include, self 27 | 28 | klass.class_eval do 29 | %w( fetch_cache set_cache expire_cache clear_cache cached? ).each do |target| 30 | alias_method_chain target, :local_cache 31 | end 32 | end 33 | end 34 | end 35 | end 36 | 37 | module ActionController 38 | class Base 39 | def local_cache_for_request 40 | ActsAsCached::LocalCache.add_to ActsAsCached::ClassMethods 41 | ActsAsCached::LocalCache.local_cache = {} 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/acts_as_cached/recipes.rb: -------------------------------------------------------------------------------- 1 | Capistrano.configuration(:must_exist).load do 2 | %w(start stop restart kill status).each do |cmd| 3 | desc "#{cmd} your memcached servers" 4 | task "memcached_#{cmd}".to_sym, :roles => :app do 5 | run "RAILS_ENV=production #{ruby} #{current_path}/script/memcached_ctl #{cmd}" 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /tasks/memcached.rake: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | require 'erb' 3 | 4 | namespace :memcached do 5 | desc "Start memcached locally" 6 | task :start do 7 | memcached config_args 8 | puts "memcached started" 9 | end 10 | 11 | desc "Restart memcached locally" 12 | task :restart do 13 | Rake::Task['memcached:stop'].invoke 14 | Rake::Task['memcached:start'].invoke 15 | end 16 | 17 | desc "Stop memcached locally" 18 | task :stop do 19 | `killall memcached` 20 | puts "memcached killed" 21 | end 22 | end 23 | 24 | def config 25 | return @config if @config 26 | config = YAML.load(ERB.new(IO.read(File.dirname(__FILE__) + '/../../../../config/memcached.yml')).result) 27 | @config = config['defaults'].merge(config['development']) 28 | end 29 | 30 | def config_args 31 | args = { 32 | '-p' => Array(config['servers']).first.split(':').last, 33 | '-c' => config['c_threshold'], 34 | '-m' => config['memory'], 35 | '-d' => '' 36 | } 37 | 38 | args.to_a * ' ' 39 | end 40 | 41 | def memcached(*args) 42 | `/usr/bin/env memcached #{args * ' '}` 43 | end 44 | -------------------------------------------------------------------------------- /test/benchmarking_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | ActsAsCached.config.clear 4 | config = YAML.load_file(File.join(File.dirname(__FILE__), '../defaults/memcached.yml.default')) 5 | config['test'] = config['development'] 6 | ActsAsCached.config = config 7 | Story.send :acts_as_cached 8 | 9 | context "When benchmarking is enabled" do 10 | specify "ActionController::Base should respond to rendering_runtime_with_memcache" do 11 | ActionController::Base.new.should.respond_to :rendering_runtime_with_memcache 12 | end 13 | 14 | specify "cachable Ruby classes should be respond to :logger" do 15 | Story.should.respond_to :logger 16 | end 17 | 18 | specify "a cached object should gain a fetch_cache with and without benchmarking methods" do 19 | Story.should.respond_to :fetch_cache_with_benchmarking 20 | Story.should.respond_to :fetch_cache_without_benchmarking 21 | end 22 | 23 | specify "cache_benchmark should yield and time any action" do 24 | ActsAsCached::Benchmarking.cache_runtime.should.equal 0.0 25 | 26 | level = Class.new { |k| def k.method_missing(*args) true end } 27 | Story.stubs(:logger).returns(level) 28 | 29 | Story.cache_benchmark("Seriously, nothing.", true) { 30 | sleep 0.01 31 | "Nothing." 32 | }.should.equal "Nothing." 33 | 34 | ActsAsCached::Benchmarking.cache_runtime.should.be > 0.0 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/cache_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "A Ruby class acting as cached (in general)" do 4 | include StoryCacheSpecSetup 5 | 6 | specify "should be able to retrieve a cached instance from the cache" do 7 | Story.get_cache(1).should.equal Story.find(1) 8 | end 9 | 10 | specify "should set to the cache if its not already set when getting" do 11 | Story.should.not.have.cached 1 12 | Story.get_cache(1).should.equal Story.find(1) 13 | Story.should.have.cached 1 14 | end 15 | 16 | specify "should not set to the cache if is already set when getting" do 17 | Story.expects(:set_cache).never 18 | Story.should.have.cached 2 19 | Story.get_cache(2).should.equal Story.find(2) 20 | Story.should.have.cached 2 21 | end 22 | 23 | specify "should be able to tell if a key is cached" do 24 | Story.is_cached?(1).should.equal false 25 | Story.should.not.have.cached 1 26 | Story.should.have.cached 2 27 | end 28 | 29 | specify "should be able to cache arbitrary methods using #caches" do 30 | Story.cache_store.expects(:get).returns(nil) 31 | Story.cache_store.expects(:set).with('Story:something_cool', :redbull, 1500) 32 | Story.caches(:something_cool).should.equal :redbull 33 | 34 | Story.cache_store.expects(:get).returns(:redbull) 35 | Story.cache_store.expects(:set).never 36 | Story.caches(:something_cool).should.equal :redbull 37 | end 38 | 39 | specify "should be able to cache arbitrary methods with arguments using #caches and :with" do 40 | with = :mongrel 41 | 42 | Story.cache_store.expects(:get).returns(nil) 43 | Story.cache_store.expects(:set).with("Story:block_on:#{with}", with, 1500) 44 | Story.caches(:block_on, :with => with).should.equal with 45 | 46 | Story.cache_store.expects(:get).with("Story:block_on:#{with}").returns(:okay) 47 | Story.cache_store.expects(:set).never 48 | Story.caches(:block_on, :with => with).should.equal :okay 49 | end 50 | 51 | specify "should be able to cache arbitrary methods with a nil argument using #caches and :with" do 52 | with = nil 53 | 54 | Story.cache_store.expects(:get).returns(nil) 55 | Story.cache_store.expects(:set).with("Story:pass_through:#{with}", :_nil, 1500) 56 | Story.caches(:pass_through, :with => with).should.equal with 57 | end 58 | 59 | specify "should be able to cache arbitrary methods with arguments using #caches and :withs" do 60 | withs = [ :first, :second ] 61 | 62 | cached_string = "first: #{withs.first} | second: #{withs.last}" 63 | 64 | Story.cache_store.expects(:get).returns(nil) 65 | Story.cache_store.expects(:set).with("Story:two_params:#{withs}", cached_string, 1500) 66 | Story.caches(:two_params, :withs => withs).should.equal cached_string 67 | 68 | Story.cache_store.expects(:get).with("Story:two_params:#{withs}").returns(:okay) 69 | Story.cache_store.expects(:set).never 70 | Story.caches(:two_params, :withs => withs).should.equal :okay 71 | end 72 | 73 | specify "should set nil when trying to set nil" do 74 | Story.set_cache(3, nil).should.equal nil 75 | Story.get_cache(3).should.equal nil 76 | end 77 | 78 | specify "should set false when trying to set false" do 79 | Story.set_cache(3, false).should.equal false 80 | Story.get_cache(3).should.equal false 81 | end 82 | 83 | specify "should be able to expire a cache key" do 84 | Story.should.have.cached 2 85 | Story.expire_cache(2).should.equal true 86 | Story.should.not.have.cached 2 87 | end 88 | 89 | specify "should return true when trying to expire the cache" do 90 | Story.should.not.have.cached 1 91 | Story.expire_cache(1).should.equal true 92 | Story.should.have.cached 2 93 | Story.expire_cache(2).should.equal true 94 | end 95 | 96 | specify "should be able to reset a cache key, returning the cached object if successful" do 97 | Story.expects(:find).with(2).returns(@story2) 98 | Story.should.have.cached 2 99 | Story.reset_cache(2).should.equal @story2 100 | Story.should.have.cached 2 101 | end 102 | 103 | specify "should be able to cache the value of a block" do 104 | Story.should.not.have.cached :block 105 | Story.get_cache(:block) { "this is a block" } 106 | Story.should.have.cached :block 107 | Story.get_cache(:block).should.equal "this is a block" 108 | end 109 | 110 | specify "should be able to define a class level ttl" do 111 | ttl = 1124 112 | Story.cache_config[:ttl] = ttl 113 | Story.cache_config[:store].expects(:set).with(Story.cache_key(1), @story, ttl) 114 | Story.get_cache(1) 115 | end 116 | 117 | specify "should be able to define a per-key ttl" do 118 | ttl = 3262 119 | Story.cache_config[:store].expects(:set).with(Story.cache_key(1), @story, ttl) 120 | Story.get_cache(1, :ttl => ttl) 121 | end 122 | 123 | specify "should be able to skip cache gets" do 124 | Story.should.have.cached 2 125 | ActsAsCached.skip_cache_gets = true 126 | Story.expects(:find).at_least_once 127 | Story.get_cache(2) 128 | ActsAsCached.skip_cache_gets = false 129 | end 130 | 131 | specify "should be able to use an arbitrary finder method via :finder" do 132 | Story.expire_cache(4) 133 | Story.cache_config[:finder] = :find_live 134 | Story.expects(:find_live).with(4).returns(false) 135 | Story.get_cache(4) 136 | end 137 | 138 | specify "should raise an exception if no finder method is found" do 139 | Story.cache_config[:finder] = :find_penguins 140 | proc { Story.get_cache(1) }.should.raise(NoMethodError) 141 | end 142 | 143 | specify "should be able to use an abitrary cache_id method via :cache_id" do 144 | Story.expire_cache(4) 145 | Story.cache_config[:cache_id] = :title 146 | story = Story.get_cache(1) 147 | story.cache_id.should.equal story.title 148 | end 149 | 150 | specify "should modify its cache key to reflect a :version option" do 151 | Story.cache_config[:version] = 'new' 152 | Story.cache_key(1).should.equal 'Story:new:1' 153 | end 154 | 155 | specify "should truncate the key normally if we dont have a namespace" do 156 | Story.stubs(:cache_namespace).returns(nil) 157 | key = "a" * 260 158 | Story.cache_key(key).length.should == 250 159 | end 160 | 161 | specify "should truncate key with length over 250, including namespace if set" do 162 | Story.stubs(:cache_namespace).returns("37-power-moves-app" ) 163 | key = "a" * 260 164 | (Story.cache_namespace + Story.cache_key(key)).length.should == (250 - 1) 165 | end 166 | 167 | specify "should raise an informative error message when trying to set_cache with a proc" do 168 | Story.cache_config[:store].expects(:set).raises(TypeError.new("Can't marshal Proc")) 169 | proc { Story.set_cache('proc:d', proc { nil }) }.should.raise(ActsAsCached::MarshalError) 170 | end 171 | end 172 | 173 | context "Passing an array of ids to get_cache" do 174 | include StoryCacheSpecSetup 175 | 176 | setup do 177 | @grab_stories = proc do 178 | @stories = Story.get_cache(1, 2, 3) 179 | end 180 | 181 | @keys = 'Story:1', 'Story:2', 'Story:3' 182 | @hash = { 183 | 'Story:1' => nil, 184 | 'Story:2' => $stories[2], 185 | 'Story:3' => nil 186 | } 187 | 188 | # TODO: doh, probably need to clean this up... 189 | @cache = $with_memcache ? CACHE : $cache 190 | 191 | @cache.expects(:get_multi).with(*@keys).returns(@hash) 192 | end 193 | 194 | specify "should try to fetch those ids using get_multi" do 195 | @grab_stories.call 196 | 197 | @stories.size.should.equal 3 198 | @stories.should.be.an.instance_of Hash 199 | @stories.each { |id, story| story.should.be.an.instance_of Story } 200 | end 201 | 202 | specify "should pass the cache miss ids to #find" do 203 | Story.expects(:find).with(%w(1 3)).returns($stories[1], $stories[3]) 204 | @grab_stories.call 205 | end 206 | end 207 | 208 | context "Passing an array of ids to get_cache using a cache which doesn't support get_multi" do 209 | include StoryCacheSpecSetup 210 | 211 | setup do 212 | @grab_stories = proc do 213 | @stories = Story.get_cache(1, 2, 3) 214 | end 215 | 216 | # TODO: doh, probably need to clean this up... 217 | @cache = $with_memcache ? CACHE : $cache 218 | end 219 | 220 | specify "should raise an exception" do 221 | class << @cache; undef :get_multi end 222 | proc { @grab_stories.call }.should.raise(ActsAsCached::NoGetMulti) 223 | end 224 | end 225 | 226 | context "A Ruby object acting as cached" do 227 | include StoryCacheSpecSetup 228 | 229 | specify "should be able to retrieve a cached version of itself" do 230 | Story.expects(:get_cache).with(1, {}).at_least_once 231 | @story.get_cache 232 | end 233 | 234 | specify "should be able to set itself to the cache" do 235 | Story.expects(:set_cache).with(1, @story, nil).at_least_once 236 | @story.set_cache 237 | end 238 | 239 | specify "should cache the value of a passed block" do 240 | @story.should.not.have.cached :block 241 | @story.get_cache(:block) { "this is a block" } 242 | @story.should.have.cached :block 243 | @story.get_cache(:block).should.equal "this is a block" 244 | end 245 | 246 | specify "should allow setting custom options by passing them to get_cache" do 247 | Story.expects(:set_cache).with('1:options', 'cached value', 1.hour) 248 | @story.get_cache(:options, :ttl => 1.hour) { 'cached value' } 249 | end 250 | 251 | specify "should be able to expire its cache" do 252 | Story.expects(:expire_cache).with(2) 253 | @story2.expire_cache 254 | end 255 | 256 | specify "should be able to reset its cache" do 257 | Story.expects(:reset_cache).with(2) 258 | @story2.reset_cache 259 | end 260 | 261 | specify "should be able to tell if it is cached" do 262 | @story.should.not.be.cached 263 | @story2.should.be.cached 264 | end 265 | 266 | specify "should be able to set itself to the cache with an arbitrary ttl" do 267 | ttl = 1500 268 | Story.expects(:set_cache).with(1, @story, ttl) 269 | @story.set_cache(ttl) 270 | end 271 | 272 | specify "should be able to cache arbitrary instance methods using caches" do 273 | Story.cache_store.expects(:get).returns(nil) 274 | Story.cache_store.expects(:set).with('Story:1:something_flashy', :molassy, 1500) 275 | @story.caches(:something_flashy).should.equal :molassy 276 | 277 | Story.cache_store.expects(:get).returns(:molassy) 278 | Story.cache_store.expects(:set).never 279 | @story.caches(:something_flashy).should.equal :molassy 280 | end 281 | end 282 | -------------------------------------------------------------------------------- /test/config_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "The global cache configuration" do 4 | # Pass in a hash to update the config. 5 | # If the first arg is a symbol, an expectation will be set. 6 | def setup_config(*args) 7 | options = args.last.is_a?(Hash) ? args.pop : {} 8 | @config[RAILS_ENV].update options.stringify_keys 9 | ActsAsCached::Config.expects(args.first) if args.first.is_a? Symbol 10 | ActsAsCached.config = @config 11 | end 12 | 13 | setup do 14 | ActsAsCached.config.clear 15 | @config = YAML.load_file('defaults/memcached.yml.default') 16 | @config['test'] = @config['development'].merge('benchmarking' => false, 'disabled' => false) 17 | end 18 | 19 | specify "should be able to set itself as the session store" do 20 | setup_config :setup_session_store, :sessions => true 21 | end 22 | 23 | specify "should be able to set itself as the fragment store" do 24 | setup_config :setup_fragment_store!, :fragments => true 25 | end 26 | 27 | specify "should construct a namespace from the environment and a config value" do 28 | setup_config 29 | ActsAsCached.config[:namespace].should.equal "app-#{RAILS_ENV}" 30 | end 31 | 32 | specify "should be able to set a global default ttl" do 33 | setup_config 34 | Story.send :acts_as_cached 35 | ActsAsCached.config[:ttl].should.not.be.nil 36 | Story.cache_config[:ttl].should.equal ActsAsCached.config[:ttl] 37 | end 38 | 39 | specify "should be able to swallow errors" do 40 | setup_config :raise_errors => false 41 | Story.send :acts_as_cached 42 | Story.stubs(:find).returns(Story.new) 43 | Story.cache_config[:store].expects(:get).raises(MemCache::MemCacheError) 44 | Story.cache_config[:store].expects(:set).returns(true) 45 | proc { Story.get_cache(1) }.should.not.raise(MemCache::MemCacheError) 46 | end 47 | 48 | specify "should not swallow marshal errors" do 49 | setup_config :raise_errors => false 50 | Story.send :acts_as_cached 51 | Story.stubs(:find).returns(Story.new) 52 | Story.cache_config[:store].expects(:get).returns(nil) 53 | Story.cache_config[:store].expects(:set).raises(TypeError.new("Some kind of Proc error")) 54 | proc { Story.get_cache(1) }.should.raise(ActsAsCached::MarshalError) 55 | end 56 | 57 | specify "should be able to re-raise errors" do 58 | setup_config :raise_errors => true 59 | Story.send :acts_as_cached 60 | Story.cache_config[:store].expects(:get).raises(MemCache::MemCacheError) 61 | proc { Story.get_cache(1) }.should.raise(MemCache::MemCacheError) 62 | end 63 | 64 | specify "should be able to enable benchmarking" do 65 | setup_config :benchmarking => true 66 | ActsAsCached.config[:benchmarking].should.equal true 67 | Story.send :acts_as_cached 68 | Story.methods.should.include 'fetch_cache_with_benchmarking' 69 | end 70 | 71 | specify "should be able to disable all caching" do 72 | setup_config :disabled => true 73 | Story.send :acts_as_cached 74 | Story.should.respond_to :fetch_cache_with_disabled 75 | ActsAsCached.config[:disabled].should.equal true 76 | end 77 | 78 | specify "should be able to use a global store other than memcache" do 79 | setup_config :store => 'HashStore' 80 | ActsAsCached.config[:store].should.equal HashStore.new 81 | Story.send :acts_as_cached 82 | Story.cache_config[:store].should.be ActsAsCached.config[:store] 83 | end 84 | 85 | specify "should be able to override the memcache-client hashing algorithm" do 86 | setup_config :fast_hash => true 87 | ActsAsCached.config[:fast_hash].should.equal true 88 | CACHE.hash_for('eatingsnotcheating').should.equal 1919 89 | end 90 | 91 | specify "should be able to override the memcache-client hashing algorithm" do 92 | setup_config :fastest_hash => true 93 | ActsAsCached.config[:fastest_hash].should.equal true 94 | CACHE.hash_for(string = 'eatingsnotcheating').should.equal string.hash 95 | end 96 | 97 | end 98 | 99 | 100 | context "The class configuration" do 101 | # Setups up the Story class with acts_as_cached using options 102 | def setup_cached(options = {}) 103 | Story.send :acts_as_cached, options 104 | end 105 | 106 | specify "should save unknown keys as options and not config" do 107 | setup_cached :pengiuns => true 108 | Story.cache_options.should.include :pengiuns 109 | Story.cache_config.should.not.include :pengiuns 110 | end 111 | 112 | specify "should be able to override the default finder" do 113 | setup_cached :finder => :find_by_title 114 | Story.cache_config[:finder].should.equal :find_by_title 115 | end 116 | 117 | specify "should be able to override the default cache_id" do 118 | setup_cached :cache_id => :title 119 | Story.cache_config[:cache_id].should.equal :title 120 | end 121 | 122 | specify "should be able to override the default finder and the cache_id using find_by" do 123 | setup_cached :find_by => :title 124 | Story.cache_config.should.not.include :find 125 | Story.cache_config[:finder].should.equal :find_by_title 126 | Story.cache_config[:cache_id].should.equal :title 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /test/disabled_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "When the cache is disabled" do 4 | setup do 5 | @story = Story.new(:id => 1, :title => "acts_as_cached 2 released!") 6 | @story2 = Story.new(:id => 2, :title => "BDD is something you can use") 7 | $stories = { 1 => @story, 2 => @story2 } 8 | 9 | config = YAML.load_file('defaults/memcached.yml.default') 10 | config['test'] = config['development'].merge('disabled' => true, 'benchmarking' => false) 11 | ActsAsCached.config = config 12 | Story.send :acts_as_cached 13 | end 14 | 15 | specify "get_cache should call through to the finder" do 16 | Story.expects(:find).at_least_once.returns(@story2) 17 | @story2.get_cache.should.equal @story2 18 | end 19 | 20 | specify "expire_cache should return true" do 21 | $cache.expects(:delete).never 22 | @story2.expire_cache.should.equal true 23 | end 24 | 25 | specify "reset_cache should return the object" do 26 | $cache.expects(:set).never 27 | Story.expects(:find).at_least_once.returns(@story2) 28 | @story2.reset_cache.should.equal @story2 29 | end 30 | 31 | specify "set_cache should just return the object" do 32 | $cache.expects(:set).never 33 | @story2.set_cache.should.equal @story2 34 | end 35 | 36 | specify "cached? should return false" do 37 | $cache.expects(:get).never 38 | @story2.should.not.be.cached 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/extensions_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | module ActsAsCached 4 | module Extensions 5 | module ClassMethods 6 | def user_defined_class_method 7 | true 8 | end 9 | end 10 | 11 | module InstanceMethods 12 | def user_defined_instance_method 13 | true 14 | end 15 | end 16 | end 17 | end 18 | 19 | context "When Extensions::ClassMethods exists" do 20 | include StoryCacheSpecSetup 21 | 22 | specify "caching classes should extend it" do 23 | Story.singleton_methods.should.include 'user_defined_class_method' 24 | end 25 | end 26 | 27 | context "When Extensions::InstanceMethods exists" do 28 | include StoryCacheSpecSetup 29 | 30 | specify "caching classes should include it" do 31 | Story.instance_methods.should.include 'user_defined_instance_method' 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/fragment_cache_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | require 'test/unit' 3 | require 'action_controller/test_process' 4 | 5 | ActionController::Routing::Routes.draw do |map| 6 | map.connect ':controller/:action/:id' 7 | end 8 | 9 | class FooController < ActionController::Base 10 | def url_for(*args) 11 | "http://#{Time.now.to_i}.foo.com" 12 | end 13 | end 14 | 15 | class BarController < ActionController::Base 16 | def page 17 | render :text => "give me my bongos" 18 | end 19 | 20 | def index 21 | render :text => "doop!" 22 | end 23 | 24 | def edit 25 | render :text => "rawk" 26 | end 27 | 28 | def trees_are_swell? 29 | true 30 | end 31 | 32 | def rescue_action(e) 33 | raise e 34 | end 35 | end 36 | 37 | class FooTemplate 38 | include ::ActionView::Helpers::CacheHelper 39 | 40 | attr_reader :controller 41 | 42 | def initialize 43 | @controller = FooController.new 44 | end 45 | end 46 | 47 | context "Fragment caching (when used with memcached)" do 48 | include FragmentCacheSpecSetup 49 | 50 | setup do 51 | @view = FooTemplate.new 52 | end 53 | 54 | specify "should be able to cache with a normal, non-keyed Rails cache calls" do 55 | _erbout = "" 56 | content = "Caching is fun!" 57 | 58 | ActsAsCached.config[:store].expects(:set).with(@view.controller.url_for.gsub('http://',''), content, ActsAsCached.config[:ttl]) 59 | 60 | @view.cache { _erbout << content } 61 | end 62 | 63 | specify "should be able to cache with a normal cache call when we don't have a default ttl" do 64 | begin 65 | _erbout = "" 66 | content = "Caching is fun!" 67 | 68 | original_ttl = ActsAsCached.config.delete(:ttl) 69 | ActsAsCached.config[:store].expects(:set).with(@view.controller.url_for.gsub('http://',''), content, 25.minutes) 70 | 71 | @view.cache { _erbout << content } 72 | ensure 73 | ActsAsCached.config[:ttl] = original_ttl 74 | end 75 | end 76 | 77 | specify "should be able to cache with a normal, keyed Rails cache calls" do 78 | _erbout = "" 79 | content = "Wow, even a key?!" 80 | key = "#{Time.now.to_i}_wow_key" 81 | 82 | ActsAsCached.config[:store].expects(:set).with(key, content, ActsAsCached.config[:ttl]) 83 | 84 | @view.cache(key) { _erbout << content } 85 | end 86 | 87 | specify "should be able to cache with new time-to-live option" do 88 | _erbout = "" 89 | content = "Time to live? TIME TO DIE!!" 90 | key = "#{Time.now.to_i}_death_key" 91 | 92 | ActsAsCached.config[:store].expects(:set).with(key, content, 60) 93 | @view.cache(key, { :ttl => 60 }) { _erbout << content } 94 | end 95 | 96 | specify "should ignore everything but time-to-live when options are present" do 97 | _erbout = "" 98 | content = "Don't mess around, here, sir." 99 | key = "#{Time.now.to_i}_mess_key" 100 | 101 | ActsAsCached.config[:store].expects(:set).with(key, content, 60) 102 | @view.cache(key, { :other_options => "for the kids", :ttl => 60 }) { _erbout << content } 103 | end 104 | 105 | specify "should be able to skip cache gets" do 106 | ActsAsCached.skip_cache_gets = true 107 | ActsAsCached.config[:store].expects(:get).never 108 | _erbout = "" 109 | @view.cache { _erbout << "Caching is fun!" } 110 | ActsAsCached.skip_cache_gets = false 111 | end 112 | end 113 | 114 | context "Action caching (when used with memcached)" do 115 | include FragmentCacheSpecSetup 116 | page_content = "give me my bongos" 117 | index_content = "doop!" 118 | edit_content = "rawk" 119 | 120 | setup do 121 | @controller = BarController.new 122 | @request = ActionController::TestRequest.new 123 | @response = ActionController::TestResponse.new 124 | end 125 | 126 | teardown do # clear the filter chain between specs to avoid chaos 127 | BarController.write_inheritable_attribute('filter_chain', []) 128 | end 129 | 130 | # little helper for prettier expections on the cache 131 | def cache_expects(method, expected_times = 1) 132 | ActsAsCached.config[:store].expects(method).times(expected_times) 133 | end 134 | 135 | specify "should cache using default ttl for a normal action cache without ttl" do 136 | BarController.caches_action :page 137 | 138 | key = 'test.host/bar/page' 139 | cache_expects(:set).with(key, page_content, ActsAsCached.config[:ttl]) 140 | get :page 141 | @response.body.should == page_content 142 | 143 | cache_expects(:read).with(key, nil).returns(page_content) 144 | get :page 145 | @response.body.should == page_content 146 | end 147 | 148 | specify "should cache using defaul ttl for normal, multiple action caches" do 149 | BarController.caches_action :page, :index 150 | 151 | cache_expects(:set).with('test.host/bar/page', page_content, ActsAsCached.config[:ttl]) 152 | get :page 153 | cache_expects(:set).with('test.host/bar', index_content, ActsAsCached.config[:ttl]) 154 | get :index 155 | end 156 | 157 | specify "should be able to action cache with ttl" do 158 | BarController.caches_action :page => { :ttl => 2.minutes } 159 | 160 | cache_expects(:set).with('test.host/bar/page', page_content, 2.minutes) 161 | get :page 162 | @response.body.should == page_content 163 | end 164 | 165 | specify "should be able to action cache multiple actions with ttls" do 166 | BarController.caches_action :index, :page => { :ttl => 5.minutes } 167 | 168 | cache_expects(:set).with('test.host/bar/page', page_content, 5.minutes) 169 | cache_expects(:set).with('test.host/bar', index_content, ActsAsCached.config[:ttl]) 170 | 171 | get :page 172 | @response.body.should == page_content 173 | 174 | get :index 175 | @response.body.should == index_content 176 | cache_expects(:read).with('test.host/bar', nil).returns(index_content) 177 | 178 | get :index 179 | end 180 | 181 | specify "should be able to action cache conditionally when passed something that returns true" do 182 | BarController.caches_action :page => { :if => :trees_are_swell? } 183 | 184 | cache_expects(:set).with('test.host/bar/page', page_content, ActsAsCached.config[:ttl]) 185 | 186 | get :page 187 | @response.body.should == page_content 188 | 189 | cache_expects(:read).with('test.host/bar/page', nil).returns(page_content) 190 | 191 | get :page 192 | end 193 | 194 | #check for edginess 195 | if [].respond_to?(:extract_options!) 196 | specify "should not break cache_path overrides" do 197 | BarController.caches_action :page, :cache_path => 'http://test.host/some/custom/path' 198 | cache_expects(:set).with('test.host/some/custom/path', page_content, ActsAsCached.config[:ttl]) 199 | get :page 200 | end 201 | 202 | specify "should not break cache_path block overrides" do 203 | BarController.caches_action :edit, :cache_path => Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]}/edit" : "http://test.host/edit" } 204 | cache_expects(:set).with('test.host/edit', edit_content, ActsAsCached.config[:ttl]) 205 | get :edit 206 | 207 | get :index 208 | cache_expects(:set).with('test.host/5/edit', edit_content, ActsAsCached.config[:ttl]) 209 | get :edit, :id => 5 210 | end 211 | 212 | specify "should play nice with custom ttls and cache_path overrides" do 213 | BarController.caches_action :page => { :ttl => 5.days }, :cache_path => 'http://test.host/my/custom/path' 214 | cache_expects(:set).with('test.host/my/custom/path', page_content, 5.days) 215 | get :page 216 | end 217 | 218 | specify "should play nice with custom ttls and cache_path block overrides" do 219 | BarController.caches_action :edit, :cache_path => Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]}/edit" : "http://test.host/edit" } 220 | cache_expects(:set).with('test.host/5/edit', edit_content, ActsAsCached.config[:ttl]) 221 | get :edit, :id => 5 222 | end 223 | 224 | specify "should play nice with the most complicated thing i can throw at it" do 225 | BarController.caches_action :index => { :ttl => 24.hours }, :page => { :ttl => 5.seconds }, :edit => { :ttl => 5.days }, :cache_path => Proc.new { |c| c.params[:id] ? "http://test.host/#{c.params[:id]}/#{c.params[:action]}" : "http://test.host/#{c.params[:action]}" } 226 | cache_expects(:set).with('test.host/index', index_content, 24.hours) 227 | get :index 228 | cache_expects(:set).with('test.host/5/edit', edit_content, 5.days) 229 | get :edit, :id => 5 230 | cache_expects(:set).with('test.host/5/page', page_content, 5.seconds) 231 | get :page, :id => 5 232 | 233 | cache_expects(:read).with('test.host/5/page', nil).returns(page_content) 234 | get :page, :id => 5 235 | cache_expects(:read).with('test.host/5/edit', nil).returns(edit_content) 236 | get :edit, :id => 5 237 | cache_expects(:read).with('test.host/index', nil).returns(index_content) 238 | get :index 239 | end 240 | end 241 | 242 | specify "should be able to skip action caching when passed something that returns false" do 243 | BarController.caches_action :page => { :if => Proc.new {|c| !c.trees_are_swell?} } 244 | 245 | cache_expects(:set, 0).with('test.host/bar/page', page_content, ActsAsCached.config[:ttl]) 246 | 247 | get :page 248 | @response.body.should == page_content 249 | end 250 | end 251 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # This file exists to fake out all the Railsisms we use so we can run the 3 | # tests in isolation. 4 | $LOAD_PATH.unshift 'lib/' 5 | # 6 | 7 | begin 8 | require 'rubygems' 9 | gem 'mocha', '>= 0.4.0' 10 | require 'mocha' 11 | gem 'test-spec', '= 0.3.0' 12 | require 'test/spec' 13 | require 'multi_rails_init' 14 | rescue LoadError 15 | puts '=> acts_as_cached tests depend on the following gems: mocha (0.4.0+), test-spec (0.3.0), multi_rails (0.0.2), and rails.' 16 | end 17 | 18 | begin 19 | require 'redgreen' 20 | rescue LoadError 21 | nil 22 | end 23 | 24 | Test::Spec::Should.send :alias_method, :have, :be 25 | Test::Spec::ShouldNot.send :alias_method, :have, :be 26 | 27 | ## 28 | # real men test without mocks 29 | if $with_memcache = ARGV.include?('with-memcache') 30 | require 'memcache' 31 | end 32 | 33 | ## 34 | # init.rb hacks 35 | RAILS_ROOT = '.' unless defined? RAILS_ROOT 36 | RAILS_ENV = 'test' unless defined? RAILS_ENV 37 | 38 | ## 39 | # get the default config using absolute path, so tests all play nice when run in isolation 40 | DEFAULT_CONFIG_FILE = File.expand_path(File.dirname(__FILE__) + '/../defaults/memcached.yml.default') 41 | 42 | ## 43 | # aac 44 | require 'acts_as_cached' 45 | Object.send :include, ActsAsCached::Mixin 46 | 47 | ## 48 | # i need you. 49 | module Enumerable 50 | def index_by 51 | inject({}) do |accum, elem| 52 | accum[yield(elem)] = elem 53 | accum 54 | end 55 | end 56 | end 57 | 58 | ## 59 | # mocky. 60 | class HashStore < Hash 61 | alias :get :[] 62 | 63 | def get_multi(*values) 64 | reject { |k,v| !values.include? k } 65 | end 66 | 67 | def set(key, value, *others) 68 | self[key] = value 69 | end 70 | 71 | def namespace 72 | nil 73 | end 74 | end 75 | 76 | $cache = HashStore.new 77 | 78 | class Story 79 | acts_as_cached($with_memcache ? {} : { :store => $cache }) 80 | 81 | attr_accessor :id, :title 82 | 83 | def initialize(attributes = {}) 84 | attributes.each { |key, value| instance_variable_set("@#{key}", value) } 85 | end 86 | 87 | def attributes 88 | { :id => id, :title => title } 89 | end 90 | 91 | def ==(other) 92 | return false unless other.respond_to? :attributes 93 | attributes == other.attributes 94 | end 95 | 96 | def self.find(*args) 97 | options = args.last.is_a?(Hash) ? args.pop : {} 98 | 99 | if (ids = args.flatten).size > 1 100 | ids.map { |id| $stories[id.to_i] } 101 | elsif (id = args.flatten.first).to_i.to_s == id.to_s 102 | $stories[id.to_i] 103 | end 104 | end 105 | 106 | def self.find_by_title(*args) 107 | title = args.shift 108 | find(args).select { |s| s.title == title } 109 | end 110 | 111 | def self.base_class 112 | Story 113 | end 114 | 115 | def self.something_cool; :redbull end 116 | def something_flashy; :molassy end 117 | 118 | def self.block_on(target = nil) 119 | target || :something 120 | end 121 | 122 | def self.pass_through(target) 123 | target 124 | end 125 | 126 | def self.two_params(first, second) 127 | "first: #{first} | second: #{second}" 128 | end 129 | 130 | def self.find_live(*args) false end 131 | end 132 | 133 | class Feature < Story; end 134 | class Interview < Story; end 135 | 136 | module ActionController 137 | class Base 138 | def rendering_runtime(*args) '' end 139 | def self.silence; yield end 140 | end 141 | end 142 | 143 | class MemCache 144 | attr_accessor :servers 145 | def initialize(*args) end 146 | class MemCacheError < StandardError; end unless defined? MemCacheError 147 | end unless $with_memcache 148 | 149 | module StoryCacheSpecSetup 150 | def self.included(base) 151 | base.setup do 152 | setup_cache_spec 153 | Story.instance_eval { @max_key_length = nil } 154 | end 155 | end 156 | 157 | def setup_cache_spec 158 | @story = Story.new(:id => 1, :title => "acts_as_cached 2 released!") 159 | @story2 = Story.new(:id => 2, :title => "BDD is something you can use") 160 | @story3 = Story.new(:id => 3, :title => "RailsConf is overrated.") 161 | $stories = { 1 => @story, 2 => @story2, 3 => @story3 } 162 | 163 | $with_memcache ? with_memcache : with_mock 164 | end 165 | 166 | def with_memcache 167 | unless $mc_setup_for_story_cache_spec 168 | ActsAsCached.config.clear 169 | config = YAML.load_file(DEFAULT_CONFIG_FILE) 170 | config['test'] = config['development'].merge('benchmarking' => false, 'disabled' => false) 171 | ActsAsCached.config = config 172 | $mc_setup_for_story_cache_spec = true 173 | end 174 | 175 | Story.send :acts_as_cached 176 | Story.expire_cache(1) 177 | Story.expire_cache(2) 178 | Story.expire_cache(3) 179 | Story.expire_cache(:block) 180 | Story.set_cache(2, @story2) 181 | end 182 | 183 | def with_mock 184 | $cache.clear 185 | 186 | Story.send :acts_as_cached, :store => $cache 187 | $cache['Story:2'] = @story2 188 | end 189 | end 190 | 191 | module FragmentCacheSpecSetup 192 | def self.included(base) 193 | base.setup { setup_fragment_spec } 194 | end 195 | 196 | def setup_fragment_spec 197 | unless $mc_setup_for_fragment_cache_spec 198 | ActsAsCached.config.clear 199 | config = YAML.load_file(DEFAULT_CONFIG_FILE) 200 | 201 | if $with_memcache 202 | other_options = { 'fragments' => true } 203 | else 204 | Object.const_set(:CACHE, $cache) unless defined? CACHE 205 | other_options = { 'fragments' => true, 'store' => $cache } 206 | end 207 | 208 | config['test'] = config['development'].merge other_options 209 | 210 | ActsAsCached.config = config 211 | ActsAsCached::FragmentCache.setup! 212 | $mc_setup_for_fragment_cache_spec = true 213 | end 214 | end 215 | end 216 | -------------------------------------------------------------------------------- /test/local_cache_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "When local_cache_for_request is called" do 4 | include StoryCacheSpecSetup 5 | 6 | setup do 7 | ActionController::Base.new.local_cache_for_request 8 | $cache = CACHE if $with_memcache 9 | end 10 | 11 | specify "get_cache should pull from the local cache on a second hit" do 12 | $cache.expects(:get).with('Story:2').returns(@story2) 13 | @story2.get_cache 14 | $cache.expects(:get).never 15 | @story2.get_cache 16 | end 17 | 18 | specify "set_cache should set to the local cache" do 19 | $cache.expects(:set).at_least_once.returns(@story) 20 | ActsAsCached::LocalCache.local_cache.expects(:[]=).with('Story:1', @story).returns(@story) 21 | @story.set_cache 22 | end 23 | 24 | specify "expire_cache should clear from the local cache" do 25 | @story2.get_cache 26 | $cache.expects(:delete).at_least_once 27 | ActsAsCached::LocalCache.local_cache.expects(:delete).with('Story:2') 28 | @story2.expire_cache 29 | end 30 | 31 | specify "clear_cache should clear from the local cache" do 32 | @story2.get_cache 33 | $cache.expects(:delete).at_least_once 34 | ActsAsCached::LocalCache.local_cache.expects(:delete).with('Story:2') 35 | @story2.clear_cache 36 | end 37 | 38 | specify "cached? should check the local cache" do 39 | @story2.get_cache 40 | $cache.expects(:get).never 41 | @story2.cached? 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/sti_test.rb: -------------------------------------------------------------------------------- 1 | require File.join(File.dirname(__FILE__), 'helper') 2 | 3 | context "An STI subclass acting as cached" do 4 | include StoryCacheSpecSetup 5 | 6 | setup do 7 | @feature = Feature.new(:id => 3, :title => 'Behind the scenes of acts_as_cached') 8 | @interview = Interview.new(:id => 4, :title => 'An interview with the Arcade Fire') 9 | @feature.expire_cache 10 | @interview.expire_cache 11 | $stories.update 3 => @feature, 4 => @interview 12 | end 13 | 14 | specify "should be just as retrievable as any other cachable Ruby object" do 15 | Feature.cached?(3).should.equal false 16 | Feature.get_cache(3) 17 | Feature.cached?(3).should.equal true 18 | end 19 | 20 | specify "should have a key corresponding to its parent class" do 21 | @feature.cache_key.should.equal "Story:3" 22 | @interview.cache_key.should.equal "Story:4" 23 | end 24 | 25 | specify "should be able to get itself from the cache via its parent class" do 26 | Story.get_cache(3).should.equal @feature 27 | Story.get_cache(4).should.equal @interview 28 | end 29 | 30 | specify "should take on its parents cache options but be able to set its own" do 31 | @feature.cache_key.should.equal "Story:3" 32 | Feature.cache_config[:version] = 1 33 | @feature.cache_key.should.equal "Story:1:3" 34 | @story.cache_key.should.equal "Story:1" 35 | end 36 | end 37 | --------------------------------------------------------------------------------