aub-cache_advance 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|