aub-cache_advance 0.2.0
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/README.textile +47 -0
- data/Rakefile +62 -0
- data/TODO.textile +6 -0
- data/cache_advance.gemspec +24 -0
- data/lib/cache_advance/active_record_sweeper.rb +32 -0
- data/lib/cache_advance/cache_set.rb +51 -0
- data/lib/cache_advance/cached_key_list.rb +46 -0
- data/lib/cache_advance/lock.rb +40 -0
- data/lib/cache_advance/mapper.rb +26 -0
- data/lib/cache_advance/named_cache.rb +120 -0
- data/lib/cache_advance/named_cache_configuration.rb +7 -0
- data/lib/cache_advance.rb +19 -0
- data/rails/init.rb +47 -0
- data/test/active_record_sweeper_test.rb +32 -0
- data/test/cache_mock.rb +15 -0
- data/test/cache_set_test.rb +39 -0
- data/test/mapper_test.rb +45 -0
- data/test/named_cache_test.rb +126 -0
- data/test/spec/db/schema.rb +18 -0
- data/test/spec/mapper_spec.rb +16 -0
- data/test/spec/mocks/memcache.rb +41 -0
- data/test/spec/spec_helper.rb +30 -0
- data/test/test_helper.rb +10 -0
- metadata +82 -0
data/README.textile
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
h1. cache advance
|
|
2
|
+
|
|
3
|
+
CacheAdvance is a wrapper around the Rails caching system that provides a simple, centralized
|
|
4
|
+
configuration file for defining caches and an even simpler way to apply them.
|
|
5
|
+
|
|
6
|
+
Written by "Aubrey Holland":mailto:aubreyholland@gmail.com.
|
|
7
|
+
|
|
8
|
+
h3. download
|
|
9
|
+
|
|
10
|
+
Github: "Page":http://github.com/aub/cache_advance/tree/master
|
|
11
|
+
|
|
12
|
+
Gem: <pre>gem install aub-cache_advance --source http://gems.github.com</pre>
|
|
13
|
+
|
|
14
|
+
Note: if you install using the gem from Github, you'll need this
|
|
15
|
+
in your environment.rb if you want to use Rails 2.1's dependency manager:
|
|
16
|
+
|
|
17
|
+
<pre><code>
|
|
18
|
+
config.gem 'aub-cache_advance', :lib => 'cache_advance', :source => 'http://gems.github.com'
|
|
19
|
+
</code></pre>
|
|
20
|
+
|
|
21
|
+
h3. cache definition
|
|
22
|
+
|
|
23
|
+
Caches are defined in the file config/caches.rb, which will be loaded automatically by the gem.
|
|
24
|
+
This file is similar in format to the rails routes config file, allowing you to specify named
|
|
25
|
+
caches and configure their keys and how they will be expired.
|
|
26
|
+
|
|
27
|
+
<pre><code>
|
|
28
|
+
CacheAdvance::Caches.define_caches do |config|
|
|
29
|
+
|
|
30
|
+
config.qualifier(:params) do |request|
|
|
31
|
+
request.params
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
config.plugin :template_handler_observer_cache_plugin
|
|
35
|
+
|
|
36
|
+
config.content_block :expiration_time => 10.minutes, :qualifiers => [ :subdomain, :params ]
|
|
37
|
+
|
|
38
|
+
config.change_towns_all :expiration_types => [ :publication ]
|
|
39
|
+
config.change_towns_limited :expiration_types => [ :publication ]
|
|
40
|
+
|
|
41
|
+
config.publication_twitter_update :expiration_time => 10.minutes, :qualifiers => [ :subdomain ]
|
|
42
|
+
|
|
43
|
+
config.weather_widget :expiration_time => 10.minutes, :qualifiers => [ :subdomain ]
|
|
44
|
+
end
|
|
45
|
+
</code></pre>
|
|
46
|
+
|
|
47
|
+
..............More later.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
require 'rake/testtask'
|
|
4
|
+
require 'rake/rdoctask'
|
|
5
|
+
require 'rake/gempackagetask'
|
|
6
|
+
require 'date'
|
|
7
|
+
|
|
8
|
+
desc 'Default: run unit tests.'
|
|
9
|
+
task :default => :test
|
|
10
|
+
|
|
11
|
+
desc 'Test the system.'
|
|
12
|
+
Rake::TestTask.new(:test) do |t|
|
|
13
|
+
t.libs << 'lib'
|
|
14
|
+
t.pattern = 'test/**/*_test.rb'
|
|
15
|
+
t.verbose = true
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
desc 'Generate documentation for the system.'
|
|
19
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
|
20
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
21
|
+
rdoc.title = 'Cache Advance'
|
|
22
|
+
rdoc.options << '--line-numbers' << '--inline-source' << "--main" << "README.textile"
|
|
23
|
+
rdoc.rdoc_files.include('README.textile')
|
|
24
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
spec = Gem::Specification.new do |s|
|
|
28
|
+
s.name = %q{cache_advance}
|
|
29
|
+
s.version = "1.0.0"
|
|
30
|
+
s.summary = %q{A system for spiffy declarative caching}
|
|
31
|
+
s.description = %q{A system for spiffy declarative caching}
|
|
32
|
+
s.homepage = 'http://github.com/aub/cache_advance'
|
|
33
|
+
|
|
34
|
+
s.files = FileList['[A-Z]*', 'lib/**/*.rb', 'rails/**/*.rb', 'test/**/*.rb']
|
|
35
|
+
s.require_path = 'lib'
|
|
36
|
+
s.test_files = Dir[*['test/**/*_test.rb']]
|
|
37
|
+
|
|
38
|
+
s.has_rdoc = true
|
|
39
|
+
s.extra_rdoc_files = ["README.textile"]
|
|
40
|
+
s.rdoc_options = ['--line-numbers', '--inline-source', "--main", "README.textile"]
|
|
41
|
+
|
|
42
|
+
s.authors = ["Aubrey Holland"]
|
|
43
|
+
s.email = %q{aubrey@patch.com}
|
|
44
|
+
|
|
45
|
+
s.platform = Gem::Platform::RUBY
|
|
46
|
+
s.add_dependency(%q<activesupport>, [">= 1.0"])
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
Rake::GemPackageTask.new spec do |pkg|
|
|
50
|
+
pkg.need_tar = true
|
|
51
|
+
pkg.need_zip = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
desc "Clean files generated by rake tasks"
|
|
55
|
+
task :clobber => [:clobber_rdoc, :clobber_package]
|
|
56
|
+
|
|
57
|
+
desc "Generate a gemspec file"
|
|
58
|
+
task :gemspec do
|
|
59
|
+
File.open("#{spec.name}.gemspec", 'w') do |f|
|
|
60
|
+
f.write spec.to_ruby
|
|
61
|
+
end
|
|
62
|
+
end
|
data/TODO.textile
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
- Fix to allow keys of > 256 characters with memcache
|
|
2
|
+
- Make test coverage not pitiful
|
|
3
|
+
- Add ability for cache to expire when a specific object changes
|
|
4
|
+
- Apply plugins only to specific caches
|
|
5
|
+
- Test with caching strategies other than memcache
|
|
6
|
+
- Improve documentation
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Gem::Specification.new do |s|
|
|
2
|
+
s.version = '0.2.0'
|
|
3
|
+
s.date = %q{2009-01-08}
|
|
4
|
+
|
|
5
|
+
s.name = %q{cache_advance}
|
|
6
|
+
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
|
|
7
|
+
s.authors = ['Aubrey Holland']
|
|
8
|
+
s.description = %q{A system for spiffy declarative caching}
|
|
9
|
+
s.email = %q{aubrey@patch.com}
|
|
10
|
+
s.files = %w(README.textile Rakefile TODO.textile cache_advance-1.0.9.gem cache_advance.gemspec lib/cache_advance.rb lib/cache_advance/active_record_sweeper.rb lib/cache_advance/cache_set.rb lib/cache_advance/cached_key_list.rb lib/cache_advance/lock.rb lib/cache_advance/mapper.rb lib/cache_advance/named_cache.rb lib/cache_advance/named_cache_configuration.rb rails/init.rb test/active_record_sweeper_test.rb test/cache_mock.rb test/cache_set_test.rb test/mapper_test.rb test/named_cache_test.rb test/spec/db/schema.rb test/spec/mapper_spec.rb test/spec/mocks/memcache.rb test/spec/spec_helper.rb test/test_helper.rb)
|
|
11
|
+
s.homepage = %q{http://github.com/aub/cache_advance}
|
|
12
|
+
s.require_paths = ['lib']
|
|
13
|
+
s.rubygems_version = %q{1.2.0}
|
|
14
|
+
s.summary = %q{A system for spiffy declarative caching}
|
|
15
|
+
s.has_rdoc = true
|
|
16
|
+
s.extra_rdoc_files = ['README.textile']
|
|
17
|
+
s.rdoc_options = ['--line-numbers', '--inline-source', '--main', 'README.textile']
|
|
18
|
+
s.test_files = %w(test/active_record_sweeper_test.rb test/cache_set_test.rb test/mapper_test.rb test/named_cache_test.rb)
|
|
19
|
+
|
|
20
|
+
if s.respond_to? :specification_version then
|
|
21
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
22
|
+
s.specification_version = 2
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
gem 'activerecord'
|
|
3
|
+
require 'active_record'
|
|
4
|
+
|
|
5
|
+
module CacheAdvance
|
|
6
|
+
class ActiveRecordSweeper < ::ActiveRecord::Observer
|
|
7
|
+
|
|
8
|
+
def self.initialize_observed(classes)
|
|
9
|
+
observe(classes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def reload_sweeper
|
|
13
|
+
observed_classes.each do |klass|
|
|
14
|
+
klass.name.constantize.add_observer(self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def after_create(object)
|
|
19
|
+
expire_caches_for(object)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias_method :after_update, :after_create
|
|
23
|
+
alias_method :after_destroy, :after_create
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def expire_caches_for(object)
|
|
28
|
+
class_symbol = object.class.name.underscore.to_sym
|
|
29
|
+
CacheAdvance.cache_set.expire_for_class(class_symbol)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module CacheAdvance
|
|
2
|
+
class CacheSet
|
|
3
|
+
attr_reader :named_caches
|
|
4
|
+
attr_reader :qualifiers
|
|
5
|
+
attr_reader :plugins
|
|
6
|
+
|
|
7
|
+
def initialize(store)
|
|
8
|
+
@store, @named_caches, @qualifiers, @plugins = store, {}, {}, []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def setup_complete
|
|
12
|
+
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def apply(cache_name, request, options, &block)
|
|
16
|
+
named_cache = @named_caches[cache_name]
|
|
17
|
+
raise UnknownNamedCacheException if named_cache.nil?
|
|
18
|
+
named_cache.value_for(request, options, &block)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def add_qualifier(name, proc)
|
|
22
|
+
@qualifiers[name] = proc
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_plugin(plugin)
|
|
26
|
+
@plugins << plugin
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_named_cache(name, options)
|
|
30
|
+
@named_caches[name] = NamedCache.new(name, options, self, @store)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def define_caches
|
|
34
|
+
yield Mapper.new(self)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def create_sweepers
|
|
38
|
+
@sweeper_type.initialize_observed(@named_caches.values.map { |c| c.expiration_types }.flatten.compact.uniq)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def expire_for_class(class_name)
|
|
42
|
+
@named_caches.values.each do |named_cache|
|
|
43
|
+
named_cache.expire_for(class_name)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def sweeper_type=(type)
|
|
48
|
+
@sweeper_type = type
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module CacheAdvance
|
|
2
|
+
class CachedKeyList
|
|
3
|
+
def initialize(store, cache_key, expiration_time=nil)
|
|
4
|
+
@store, @cache_key, @expiration_time = store, cache_key, expiration_time
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def all_keys
|
|
8
|
+
key_list.keys
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def add_key(key)
|
|
12
|
+
Lock.new(@store).execute_locked(@cache_key) do
|
|
13
|
+
data = key_list
|
|
14
|
+
unless data.has_key?(key)
|
|
15
|
+
data[key] = @expiration_time.nil? ? nil : Time.now + @expiration_time
|
|
16
|
+
@store.set(@cache_key, data)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def delete_key(key)
|
|
22
|
+
Lock.new(@store).execute_locked(@cache_key) do
|
|
23
|
+
data = key_list
|
|
24
|
+
if data.has_key?(key)
|
|
25
|
+
data.delete(key)
|
|
26
|
+
@store.set(@cache_key, data)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def clear
|
|
32
|
+
@store.set(@cache_key, {})
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
def key_list
|
|
38
|
+
list = @store.get(@cache_key) || {}
|
|
39
|
+
if @expiration_time
|
|
40
|
+
now = Time.now
|
|
41
|
+
list.delete_if { |k,v| v <= now }
|
|
42
|
+
end
|
|
43
|
+
list
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module CacheAdvance
|
|
2
|
+
class Lock
|
|
3
|
+
|
|
4
|
+
class LockAcquisitionFailureException < Exception; end
|
|
5
|
+
|
|
6
|
+
DEFAULT_RETRIES = 5
|
|
7
|
+
DEFAULT_EXPIRATION_TIME = 30
|
|
8
|
+
|
|
9
|
+
def initialize(store)
|
|
10
|
+
@store = store
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def execute_locked(key, lock_expiry = DEFAULT_EXPIRATION_TIME, retries = DEFAULT_RETRIES)
|
|
14
|
+
begin
|
|
15
|
+
acquire(key, lock_expiry, retries)
|
|
16
|
+
yield
|
|
17
|
+
ensure
|
|
18
|
+
release_lock(key)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def acquire(key, lock_expiry = DEFAULT_EXPIRATION_TIME, retries = DEFAULT_RETRIES)
|
|
23
|
+
retries.times do |count|
|
|
24
|
+
begin
|
|
25
|
+
return if @store.set("lock/#{key}", Process.pid, lock_expiry) == "STORED\r\n"
|
|
26
|
+
end
|
|
27
|
+
exponential_sleep(count) unless count == retries - 1
|
|
28
|
+
end
|
|
29
|
+
raise LockAcquisitionFailureException, "Couldn't acquire memcache lock for: #{key}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def release_lock(key)
|
|
33
|
+
@store.delete("lock/#{key}")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def exponential_sleep(count)
|
|
37
|
+
@runtime += Benchmark::measure { sleep((2**count) / 10.0) }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module CacheAdvance
|
|
2
|
+
class Mapper
|
|
3
|
+
def initialize(cache_set)
|
|
4
|
+
@cache_set = cache_set
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def qualifier(name, &proc)
|
|
8
|
+
@cache_set.add_qualifier(name, proc)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def plugin(name)
|
|
12
|
+
if name.is_a?(Symbol)
|
|
13
|
+
plugin = name.to_s.camelcase.constantize.new
|
|
14
|
+
elsif name.is_a?(Class)
|
|
15
|
+
plugin = name.new
|
|
16
|
+
else
|
|
17
|
+
plugin = name
|
|
18
|
+
end
|
|
19
|
+
@cache_set.add_plugin(plugin)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def method_missing(method, options={})
|
|
23
|
+
@cache_set.add_named_cache(method, options)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
module CacheAdvance
|
|
2
|
+
class NamedCache
|
|
3
|
+
|
|
4
|
+
ENABLED_CHECK_INTERVAL = 60
|
|
5
|
+
|
|
6
|
+
def initialize(name, params, cache_set, store)
|
|
7
|
+
@name = name.to_s
|
|
8
|
+
@params = params
|
|
9
|
+
@cache_set = cache_set
|
|
10
|
+
@store = store
|
|
11
|
+
@cached_key_list = CachedKeyList.new(@store, "#{@name}/STORED_CACHES", expiration_time)
|
|
12
|
+
@enabled_check_time = Time.now + ENABLED_CHECK_INTERVAL
|
|
13
|
+
@enabled = nil
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def value_for(request, options, &block)
|
|
17
|
+
return block.call unless enabled?
|
|
18
|
+
|
|
19
|
+
key = key_for(request, options[:key])
|
|
20
|
+
|
|
21
|
+
if (value = read_from_store(key))
|
|
22
|
+
each_plugin { |p| p.send('after_read', @name, key, request, value) if p.respond_to?('after_read') }
|
|
23
|
+
return value
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
each_plugin { |p| p.send('before_render', @name, key, request) if p.respond_to?('before_render') }
|
|
27
|
+
result = block.call
|
|
28
|
+
each_plugin { |p| p.send('after_render', @name, key, request, result) if p.respond_to?('after_render') }
|
|
29
|
+
each_plugin { |p| p.send('before_write', @name, key, request, result) if p.respond_to?('before_write') }
|
|
30
|
+
write_to_store(key, result)
|
|
31
|
+
each_plugin { |p| p.send('after_write', @name, key, request, result) if p.respond_to?('after_write') }
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def expire_for(type)
|
|
36
|
+
if expiration_types.include?(type)
|
|
37
|
+
expire_all
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def expire_all
|
|
42
|
+
delete_all_from_store
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def all_cached_keys
|
|
46
|
+
@cached_key_list.all_keys
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def expiration_types
|
|
50
|
+
Array(@params[:expiration_types])
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def title
|
|
54
|
+
@params[:title] || @name.to_s
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def enabled=(state)
|
|
58
|
+
@enabled = !!state
|
|
59
|
+
write_to_store(enabled_key, @enabled, false)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def enabled?
|
|
63
|
+
if @enabled.nil? || Time.now >= @enabled_check_time
|
|
64
|
+
@enabled = [nil, true].include?(read_from_store(enabled_key))
|
|
65
|
+
@enabled_check_time = Time.now + ENABLED_CHECK_INTERVAL
|
|
66
|
+
end
|
|
67
|
+
@enabled
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
protected
|
|
71
|
+
|
|
72
|
+
def read_from_store(key)
|
|
73
|
+
@store.get(key)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def write_to_store(key, value, add_to_key_list=true)
|
|
77
|
+
expiration_time ? @store.set(key, value, expiration_time) : @store.set(key, value)
|
|
78
|
+
if add_to_key_list
|
|
79
|
+
@cached_key_list.add_key(key)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def delete_from_store(key)
|
|
84
|
+
@store.delete(key)
|
|
85
|
+
@cached_key_list.delete_key(key)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_all_from_store
|
|
89
|
+
@cached_key_list.all_keys.each { |key| delete_from_store(key) }
|
|
90
|
+
@cached_key_list.clear
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def each_plugin
|
|
94
|
+
@cache_set.plugins.each do |p|
|
|
95
|
+
yield p
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def key_for(request, suffix='')
|
|
100
|
+
qualifier_data = qualifiers.map do |q|
|
|
101
|
+
if (qualifier = @cache_set.qualifiers[q])
|
|
102
|
+
(qualifier.call(request) || '').to_s
|
|
103
|
+
end
|
|
104
|
+
end.join('/')
|
|
105
|
+
"#{@name}/#{suffix}/[#{qualifier_data}]"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def enabled_key
|
|
109
|
+
"#{@name}/ENABLED_STATUS"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def expiration_time
|
|
113
|
+
@params[:expiration_time]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def qualifiers
|
|
117
|
+
Array(@params[:qualifiers])
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'cache_advance/cache_set'
|
|
2
|
+
require 'cache_advance/cached_key_list'
|
|
3
|
+
require 'cache_advance/mapper'
|
|
4
|
+
require 'cache_advance/named_cache'
|
|
5
|
+
require 'cache_advance/named_cache_configuration'
|
|
6
|
+
|
|
7
|
+
module CacheAdvance
|
|
8
|
+
class UnknownNamedCacheException < Exception; end
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :cache_set
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.define_caches(store)
|
|
15
|
+
@cache_set = CacheSet.new(store)
|
|
16
|
+
yield Mapper.new(@cache_set)
|
|
17
|
+
@cache_set.setup_complete # This allows the cache set to finalize some of its configuration
|
|
18
|
+
end
|
|
19
|
+
end
|
data/rails/init.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
require 'cache_advance'
|
|
2
|
+
require 'cache_advance/active_record_sweeper'
|
|
3
|
+
|
|
4
|
+
require "#{RAILS_ROOT}/config/caches"
|
|
5
|
+
require 'dispatcher'
|
|
6
|
+
|
|
7
|
+
# Setup the sweeper and cache types as appropriate for Rails.
|
|
8
|
+
CacheAdvance.cache_set.sweeper_type = CacheAdvance::ActiveRecordSweeper
|
|
9
|
+
|
|
10
|
+
# This is the helper method that can be used in rails views/controllers/helpers.
|
|
11
|
+
# If caching is disabled, just make it yield the results of the block.
|
|
12
|
+
if config.action_controller.perform_caching
|
|
13
|
+
ActionController::Base.helper do
|
|
14
|
+
def cache_it(cache, options={}, &block)
|
|
15
|
+
CacheAdvance.cache_set.apply(cache, request, options) do
|
|
16
|
+
capture(&block)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
else
|
|
21
|
+
ActionController::Base.helper do
|
|
22
|
+
def cache_it(cache, options={}, &block)
|
|
23
|
+
capture(&block)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
ActionMailer::Base.helper do
|
|
29
|
+
def cache_it(cache, options={}, &block)
|
|
30
|
+
capture(&block)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# This will get called after the standard rails environment is initialized.
|
|
35
|
+
config.after_initialize do
|
|
36
|
+
if config.action_controller.perform_caching
|
|
37
|
+
# This hooks the sweepers into the observer system and adds it to the list.
|
|
38
|
+
CacheAdvance.cache_set.create_sweepers
|
|
39
|
+
ActiveRecord::Base.observers << CacheAdvance::ActiveRecordSweeper
|
|
40
|
+
|
|
41
|
+
# In development mode, the models we observe get reloaded with each request. Using
|
|
42
|
+
# this hook allows us to reload the observer relationships each time as well.
|
|
43
|
+
ActionController::Dispatcher.to_prepare(:cache_advance_reload) do
|
|
44
|
+
CacheAdvance::ActiveRecordSweeper.instance.reload_sweeper
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
require 'cache_advance/active_record_sweeper'
|
|
4
|
+
|
|
5
|
+
class Article; end
|
|
6
|
+
class Publication; end
|
|
7
|
+
|
|
8
|
+
class ActiveRecordSweeperTest < Test::Unit::TestCase
|
|
9
|
+
|
|
10
|
+
def setup
|
|
11
|
+
@sweeper = CacheAdvance::ActiveRecordSweeper.instance
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_should_call_observe_with_a_given_set_of_classes
|
|
15
|
+
CacheAdvance::ActiveRecordSweeper.expects(:observe).with([:publication, :article])
|
|
16
|
+
CacheAdvance::ActiveRecordSweeper.initialize_observed([:publication, :article])
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_should_re_add_observers
|
|
20
|
+
CacheAdvance::ActiveRecordSweeper.initialize_observed([:publication, :article])
|
|
21
|
+
Article.expects(:add_observer).with(@sweeper)
|
|
22
|
+
Publication.expects(:add_observer).with(@sweeper)
|
|
23
|
+
@sweeper.reload_sweeper
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_should_expire_caches_on_changes
|
|
27
|
+
CacheAdvance::Caches.expects(:expire_for_class).with(:publication).times(3)
|
|
28
|
+
%w(after_create after_update after_destroy).each do |method|
|
|
29
|
+
@sweeper.send(method, Publication.new)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/test/cache_mock.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class Plugin; end
|
|
4
|
+
|
|
5
|
+
require 'cache_advance/active_record_sweeper'
|
|
6
|
+
|
|
7
|
+
class CacheSetTest < Test::Unit::TestCase
|
|
8
|
+
def setup
|
|
9
|
+
@cache_set = CacheAdvance::CacheSet.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def test_define_caches_should_yield_a_mapper
|
|
13
|
+
@cache_set.define_caches do |mapper|
|
|
14
|
+
assert_equal CacheAdvance::Mapper, mapper.class
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_should_apply_the_cache_if_found
|
|
19
|
+
request = mock
|
|
20
|
+
options = { :key => 'hippo' }
|
|
21
|
+
@cache_set.add_named_cache(:kewl, {})
|
|
22
|
+
@cache_set.named_caches[:kewl].expects(:value_for).with(request, options)
|
|
23
|
+
@cache_set.apply(:kewl, request, options) { }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def test_apply_should_throw_exception_with_invalid_name
|
|
27
|
+
assert_raise CacheAdvance::UnknownNamedCacheException do
|
|
28
|
+
@cache_set.apply(:total_hack, mock(), {}) { }
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_should_pass_expiration_types_to_the_sweeper
|
|
33
|
+
@cache_set.sweeper_type = CacheAdvance::ActiveRecordSweeper
|
|
34
|
+
@cache_set.add_named_cache(:kewl, { :expiration_types => [:publication, :article] })
|
|
35
|
+
@cache_set.add_named_cache(:howza, { :expiration_types => [:publication] })
|
|
36
|
+
CacheAdvance::ActiveRecordSweeper.expects(:initialize_observed).with([:publication, :article])
|
|
37
|
+
@cache_set.create_sweepers
|
|
38
|
+
end
|
|
39
|
+
end
|
data/test/mapper_test.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class Hack
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class MapperTest < Test::Unit::TestCase
|
|
7
|
+
|
|
8
|
+
def setup
|
|
9
|
+
@cache_set = CacheAdvance::CacheSet.new
|
|
10
|
+
@mapper = CacheAdvance::Mapper.new(@cache_set)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def test_qualifier
|
|
14
|
+
@mapper.qualifier(:thirty_four) do
|
|
15
|
+
34
|
|
16
|
+
end
|
|
17
|
+
assert_equal 1, @cache_set.qualifiers.size
|
|
18
|
+
assert_equal 34, @cache_set.qualifiers[:thirty_four].call
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_plugin_from_symbol
|
|
22
|
+
@mapper.plugin(:hack)
|
|
23
|
+
assert_equal 1, @cache_set.plugins.size
|
|
24
|
+
assert_equal Hack, @cache_set.plugins.first.class
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_plugin_from_class
|
|
28
|
+
@mapper.plugin(Hack)
|
|
29
|
+
assert_equal 1, @cache_set.plugins.size
|
|
30
|
+
assert_equal Hack, @cache_set.plugins.first.class
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def test_plugin_from_object
|
|
34
|
+
hack = Hack.new
|
|
35
|
+
@mapper.plugin(hack)
|
|
36
|
+
assert_equal 1, @cache_set.plugins.size
|
|
37
|
+
assert_equal hack, @cache_set.plugins.first
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def test_adding_caches_through_method_missing
|
|
41
|
+
@mapper.say_what :option => 2
|
|
42
|
+
assert_equal 1, @cache_set.named_caches.size
|
|
43
|
+
assert_equal CacheAdvance::NamedCache, @cache_set.named_caches[:say_what].class
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper.rb'
|
|
2
|
+
|
|
3
|
+
class NamedCacheTest < Test::Unit::TestCase
|
|
4
|
+
|
|
5
|
+
def setup
|
|
6
|
+
@request = mock
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_key_for_in_basic_case
|
|
10
|
+
create_named_cache
|
|
11
|
+
assert_equal 'test_cache', @named_cache.key_for(@request)
|
|
12
|
+
assert_equal 'test_cachesuf', @named_cache.key_for(@request, 'suf')
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_key_with_qualifiers
|
|
16
|
+
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
protected
|
|
20
|
+
|
|
21
|
+
def create_named_cache(options={})
|
|
22
|
+
@name = options[:name] || 'test_cache'
|
|
23
|
+
@params = {}
|
|
24
|
+
@cache_set = CacheAdvance::CacheSet.new
|
|
25
|
+
@cache = CacheAdvance::CacheMock.new
|
|
26
|
+
@named_cache = CacheAdvance::NamedCache.new(@name, @params, @cache_set, @cache)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# module CacheAdvance
|
|
32
|
+
# class NamedCache
|
|
33
|
+
# STORED_KEY = 'STORED_CACHES'
|
|
34
|
+
#
|
|
35
|
+
# def initialize(name, params, configuration, cache)
|
|
36
|
+
# @name = name.to_s
|
|
37
|
+
# @params = params
|
|
38
|
+
# @configuration = configuration
|
|
39
|
+
# @cache = cache
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# def key_for(request, suffix='')
|
|
43
|
+
# key = @name.dup
|
|
44
|
+
# key << suffix.to_s
|
|
45
|
+
#
|
|
46
|
+
# qualifiers.each do |q|
|
|
47
|
+
# if (qualifier = @configuration.qualifiers[q])
|
|
48
|
+
# this_one = qualifier.call(request)
|
|
49
|
+
# key << this_one.to_s unless this_one.nil?
|
|
50
|
+
# end
|
|
51
|
+
# end if qualifiers
|
|
52
|
+
# key
|
|
53
|
+
# end
|
|
54
|
+
#
|
|
55
|
+
# def value_for(request, options, &block)
|
|
56
|
+
# key = key_for(request, options[:key])
|
|
57
|
+
#
|
|
58
|
+
# if (cache = @cache.read(key))
|
|
59
|
+
# call_plugins('after_read', key, request)
|
|
60
|
+
# return cache
|
|
61
|
+
# end
|
|
62
|
+
#
|
|
63
|
+
# call_plugins('before_write', key, request)
|
|
64
|
+
# result = block.call
|
|
65
|
+
# @cache.write(key, result, rails_options)
|
|
66
|
+
# call_plugins('after_write', key, request)
|
|
67
|
+
#
|
|
68
|
+
# add_to_cached_keys_list(key)
|
|
69
|
+
#
|
|
70
|
+
# result
|
|
71
|
+
# end
|
|
72
|
+
#
|
|
73
|
+
# def rails_options
|
|
74
|
+
# options = {}
|
|
75
|
+
# options[:expires_in] = expiration_time if expiration_time
|
|
76
|
+
# options
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# def expire_for(type)
|
|
80
|
+
# if expiration_types.include?(type)
|
|
81
|
+
# expire_all
|
|
82
|
+
# end
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# def expire_all
|
|
86
|
+
# if (data = @cache.read(@name + STORED_KEY))
|
|
87
|
+
# data = Array(Marshal.load(data))
|
|
88
|
+
# data.each { |key| @cache.delete(key) }
|
|
89
|
+
# else
|
|
90
|
+
# @cache.delete(@name)
|
|
91
|
+
# end
|
|
92
|
+
# end
|
|
93
|
+
#
|
|
94
|
+
# def expiration_types
|
|
95
|
+
# Array(@params[:expiration_types])
|
|
96
|
+
# end
|
|
97
|
+
#
|
|
98
|
+
# def expiration_time
|
|
99
|
+
# @params[:expiration_time]
|
|
100
|
+
# end
|
|
101
|
+
#
|
|
102
|
+
# def qualifiers
|
|
103
|
+
# Array(@params[:qualifiers])
|
|
104
|
+
# end
|
|
105
|
+
#
|
|
106
|
+
# protected
|
|
107
|
+
#
|
|
108
|
+
# def call_plugins(method, key, request)
|
|
109
|
+
# @configuration.plugins.each { |p| p.send(method, @name, key, request) if p.respond_to?(method) }
|
|
110
|
+
# end
|
|
111
|
+
#
|
|
112
|
+
# def add_to_cached_keys_list(key)
|
|
113
|
+
# unless expiration_types.blank? || key == @name
|
|
114
|
+
# if (data = @cache.read(@name + STORED_KEY))
|
|
115
|
+
# data = Array(Marshal.load(data))
|
|
116
|
+
# else
|
|
117
|
+
# data = []
|
|
118
|
+
# end
|
|
119
|
+
# unless data.include?(key)
|
|
120
|
+
# @cache.write(@name + STORED_KEY, Marshal.dump(data << key))
|
|
121
|
+
# end
|
|
122
|
+
# end
|
|
123
|
+
# end
|
|
124
|
+
# end
|
|
125
|
+
# end
|
|
126
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
|
2
|
+
create_table "publications", :force => true do |t|
|
|
3
|
+
t.string "name", "subdomain"
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
create_table "articles", :force => true do |t|
|
|
7
|
+
t.references 'publication'
|
|
8
|
+
t.string 'title'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class Publication < ActiveRecord::Base
|
|
13
|
+
has_many :articles
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Article < ActiveRecord::Base
|
|
17
|
+
belongs_to :publication
|
|
18
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
|
2
|
+
|
|
3
|
+
module CacheAdvance
|
|
4
|
+
before :each do
|
|
5
|
+
@cache_set = CacheSet.new
|
|
6
|
+
@mapper = Mapper.new(@cache_set)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe 'Mapper' do
|
|
10
|
+
it 'should pass qualifiers directly to the cache set' do
|
|
11
|
+
proc = Proc.new { 2 }
|
|
12
|
+
@cache_set.should_receive(:add_qualifier).with(proc)
|
|
13
|
+
@mapper.qualifier(:testy, proc)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
class MemCache
|
|
2
|
+
attr_reader :data
|
|
3
|
+
|
|
4
|
+
def initialize
|
|
5
|
+
@data = {}
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def decr(key, amount = 1)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def get(key, raw = false)
|
|
12
|
+
return @data[key]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def get_multi(*keys)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def incr(key, amount = 1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def set(key, value, expiry = 0, raw = false)
|
|
22
|
+
@data[key] = value
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add(key, value, expiry = 0, raw = false)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def delete(key, expiry = 0)
|
|
29
|
+
@data.delete(key)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def flush_all
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def reset
|
|
36
|
+
@data = {}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def stats
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
$: << File.join(File.dirname(__FILE__), '..', 'lib')
|
|
2
|
+
$: << File.join(File.dirname(__FILE__))
|
|
3
|
+
|
|
4
|
+
require 'rubygems'
|
|
5
|
+
require 'ruby-debug'
|
|
6
|
+
|
|
7
|
+
gem 'sqlite3-ruby'
|
|
8
|
+
require 'spec'
|
|
9
|
+
require 'cache_advance'
|
|
10
|
+
|
|
11
|
+
require 'activerecord'
|
|
12
|
+
|
|
13
|
+
ActiveRecord::Base.establish_connection(
|
|
14
|
+
:adapter => 'sqlite3',
|
|
15
|
+
:dbfile => ':memory:'
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
require File.join(File.dirname(__FILE__), 'db', 'schema')
|
|
19
|
+
require File.join(File.dirname(__FILE__), 'mocks', 'memcache')
|
|
20
|
+
|
|
21
|
+
Spec::Runner.configure do |config|
|
|
22
|
+
# config.mock_with :rr
|
|
23
|
+
config.before :each do
|
|
24
|
+
@memcache = MemCache.new
|
|
25
|
+
|
|
26
|
+
CacheAdvance.define_caches(@memcache) do |cache_config|
|
|
27
|
+
cache_config.publication
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: aub-cache_advance
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aubrey Holland
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-01-08 00:00:00 -08:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description: A system for spiffy declarative caching
|
|
17
|
+
email: aubrey@patch.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- README.textile
|
|
24
|
+
files:
|
|
25
|
+
- README.textile
|
|
26
|
+
- Rakefile
|
|
27
|
+
- TODO.textile
|
|
28
|
+
- cache_advance-1.0.9.gem
|
|
29
|
+
- cache_advance.gemspec
|
|
30
|
+
- lib/cache_advance.rb
|
|
31
|
+
- lib/cache_advance/active_record_sweeper.rb
|
|
32
|
+
- lib/cache_advance/cache_set.rb
|
|
33
|
+
- lib/cache_advance/cached_key_list.rb
|
|
34
|
+
- lib/cache_advance/lock.rb
|
|
35
|
+
- lib/cache_advance/mapper.rb
|
|
36
|
+
- lib/cache_advance/named_cache.rb
|
|
37
|
+
- lib/cache_advance/named_cache_configuration.rb
|
|
38
|
+
- rails/init.rb
|
|
39
|
+
- test/active_record_sweeper_test.rb
|
|
40
|
+
- test/cache_mock.rb
|
|
41
|
+
- test/cache_set_test.rb
|
|
42
|
+
- test/mapper_test.rb
|
|
43
|
+
- test/named_cache_test.rb
|
|
44
|
+
- test/spec/db/schema.rb
|
|
45
|
+
- test/spec/mapper_spec.rb
|
|
46
|
+
- test/spec/mocks/memcache.rb
|
|
47
|
+
- test/spec/spec_helper.rb
|
|
48
|
+
- test/test_helper.rb
|
|
49
|
+
has_rdoc: true
|
|
50
|
+
homepage: http://github.com/aub/cache_advance
|
|
51
|
+
post_install_message:
|
|
52
|
+
rdoc_options:
|
|
53
|
+
- --line-numbers
|
|
54
|
+
- --inline-source
|
|
55
|
+
- --main
|
|
56
|
+
- README.textile
|
|
57
|
+
require_paths:
|
|
58
|
+
- lib
|
|
59
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: "0"
|
|
64
|
+
version:
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: "0"
|
|
70
|
+
version:
|
|
71
|
+
requirements: []
|
|
72
|
+
|
|
73
|
+
rubyforge_project:
|
|
74
|
+
rubygems_version: 1.2.0
|
|
75
|
+
signing_key:
|
|
76
|
+
specification_version: 2
|
|
77
|
+
summary: A system for spiffy declarative caching
|
|
78
|
+
test_files:
|
|
79
|
+
- test/active_record_sweeper_test.rb
|
|
80
|
+
- test/cache_set_test.rb
|
|
81
|
+
- test/mapper_test.rb
|
|
82
|
+
- test/named_cache_test.rb
|