djmaze-arid_cache 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +26 -0
- data/Gemfile +18 -0
- data/LICENSE +20 -0
- data/README.rdoc +394 -0
- data/Rakefile +78 -0
- data/VERSION +1 -0
- data/arid_cache.gemspec +104 -0
- data/init.rb +6 -0
- data/lib/arid_cache/active_record.rb +95 -0
- data/lib/arid_cache/cache_proxy.rb +368 -0
- data/lib/arid_cache/helpers.rb +86 -0
- data/lib/arid_cache/store.rb +125 -0
- data/lib/arid_cache.rb +47 -0
- data/rails/init.rb +1 -0
- data/spec/arid_cache/arid_cache_spec.rb +39 -0
- data/spec/arid_cache/cache_proxy_result_spec.rb +53 -0
- data/spec/arid_cache/cache_proxy_spec.rb +95 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/support/ar_query.rb +128 -0
- data/spec/support/custom_methods.rb +7 -0
- data/spec/support/matchers.rb +33 -0
- data/test/arid_cache_test.rb +414 -0
- data/test/console +15 -0
- data/test/lib/add_query_counting_to_active_record.rb +11 -0
- data/test/lib/blueprint.rb +29 -0
- data/test/lib/db_prepare.rb +34 -0
- data/test/lib/fix_active_support_file_store_expires_in.rb +18 -0
- data/test/lib/mock_rails.rb +29 -0
- data/test/lib/models/company.rb +6 -0
- data/test/lib/models/empty_user_relation.rb +5 -0
- data/test/lib/models/user.rb +32 -0
- data/test/log/.gitignore +0 -0
- data/test/test_helper.rb +19 -0
- metadata +177 -0
@@ -0,0 +1,86 @@
|
|
1
|
+
module AridCache
|
2
|
+
module Helpers
|
3
|
+
|
4
|
+
# Lookup something from the cache.
|
5
|
+
#
|
6
|
+
# If no block is provided, create one dynamically. If a block is
|
7
|
+
# provided, it is only used the first time it is encountered.
|
8
|
+
# This allows you to dynamically define your caches while still
|
9
|
+
# returning the results of your query.
|
10
|
+
#
|
11
|
+
# @return a WillPaginate::Collection if the options include :page,
|
12
|
+
# a Fixnum count if the request is for a count or the results of
|
13
|
+
# the ActiveRecord query otherwise.
|
14
|
+
def lookup(object, key, opts, &block)
|
15
|
+
if !block.nil?
|
16
|
+
define(object, key, opts, &block)
|
17
|
+
elsif key =~ /(.*)_count$/
|
18
|
+
if AridCache.store.has?(object, $1)
|
19
|
+
method_for_cached(object, $1, :fetch_count, key)
|
20
|
+
elsif object.respond_to?(key)
|
21
|
+
define(object, key, opts, :fetch_count)
|
22
|
+
elsif object.respond_to?($1)
|
23
|
+
define(object, $1, opts, :fetch_count, key)
|
24
|
+
else
|
25
|
+
raise ArgumentError.new("#{object} doesn't respond to #{key} or #{$1}! Cannot dynamically create query to get the count, please call with a block.")
|
26
|
+
end
|
27
|
+
elsif AridCache.store.has?(object, key)
|
28
|
+
method_for_cached(object, key, :fetch)
|
29
|
+
elsif object.respond_to?(key)
|
30
|
+
define(object, key, opts, &block)
|
31
|
+
else
|
32
|
+
raise ArgumentError.new("#{object} doesn't respond to #{key}! Cannot dynamically create query, please call with a block.")
|
33
|
+
end
|
34
|
+
object.send("cached_#{key}", opts)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Store the options and optional block for a call to the cache.
|
38
|
+
#
|
39
|
+
# If no block is provided, create one dynamically.
|
40
|
+
#
|
41
|
+
# @return an AridCache::Store::Blueprint.
|
42
|
+
def define(object, key, opts, fetch_method=:fetch, method_name=nil, &block)
|
43
|
+
|
44
|
+
# FIXME: Pass default options to store.add
|
45
|
+
# Pass nil in for now until we get the cache_ calls working.
|
46
|
+
# This means that the first time you define a dynamic cache
|
47
|
+
# (by passing in a block), the options you used are not
|
48
|
+
# stored in the blueprint and applied to each subsequent call.
|
49
|
+
#
|
50
|
+
# Otherwise we have a situation where a :limit passed in to the
|
51
|
+
# first call persists when no options are passed in on subsequent calls,
|
52
|
+
# but if a different :limit is passed in that limit is applied.
|
53
|
+
#
|
54
|
+
# I think in this scenario one would expect no limit to be applied
|
55
|
+
# if no options are passed in.
|
56
|
+
#
|
57
|
+
# When the cache_ methods are supported, those options should be
|
58
|
+
# remembered and applied to the collection however.
|
59
|
+
blueprint = AridCache.store.add_object_cache_configuration(object, key, nil, block)
|
60
|
+
method_for_cached(object, key, fetch_method, method_name)
|
61
|
+
blueprint
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
# Dynamically define a method on the given object or class to return cached results
|
67
|
+
def method_for_cached(object_or_class, key, fetch_method=:fetch, method_name=nil)
|
68
|
+
method_name = ("cached_" + (method_name || key)).gsub(/[^\w\!\?]/, '_')
|
69
|
+
method_body = <<-END
|
70
|
+
def #{method_name}(*args, &block)
|
71
|
+
opts = args.empty? ? {} : args.first
|
72
|
+
|
73
|
+
proxy = AridCache::CacheProxy.new(self, #{key.inspect}, opts, &block)
|
74
|
+
if opts[:clear] == true
|
75
|
+
proxy.clear_cached
|
76
|
+
else
|
77
|
+
proxy.#{fetch_method.to_s}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
END
|
81
|
+
# Get the correct class
|
82
|
+
klass = object_or_class.is_a?(Class) ? object_or_class : object_or_class.class
|
83
|
+
klass.class_eval(method_body, __FILE__, __LINE__)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
module AridCache
|
2
|
+
class Store < Hash
|
3
|
+
|
4
|
+
# AridCache::Store::Blueprint
|
5
|
+
#
|
6
|
+
# Stores options and blocks that are used to generate results for finds
|
7
|
+
# and counts.
|
8
|
+
Blueprint = Struct.new(:klass, :key, :opts, :proc) do
|
9
|
+
|
10
|
+
def initialize(klass, key, opts={}, proc=nil)
|
11
|
+
self.key = key
|
12
|
+
self.klass = klass
|
13
|
+
self.proc = proc
|
14
|
+
self.opts = opts
|
15
|
+
end
|
16
|
+
|
17
|
+
def klass=(object) # store the class name only
|
18
|
+
self['klass'] = object.is_a?(Class) ? object.name : object.class.name
|
19
|
+
end
|
20
|
+
|
21
|
+
def klass
|
22
|
+
self['klass'].constantize unless self['klass'].nil?
|
23
|
+
end
|
24
|
+
|
25
|
+
def opts=(value)
|
26
|
+
self['opts'] = value.symbolize_keys! unless !value.respond_to?(:symbolize_keys)
|
27
|
+
end
|
28
|
+
|
29
|
+
def opts
|
30
|
+
self['opts'] || {}
|
31
|
+
end
|
32
|
+
|
33
|
+
def proc(object=nil)
|
34
|
+
if self['proc'].nil? && !object.nil?
|
35
|
+
self['proc'] = key
|
36
|
+
else
|
37
|
+
self['proc']
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Capture cache configurations and blocks and store them in the store.
|
44
|
+
#
|
45
|
+
# A block is evaluated within the scope of this class. The blocks
|
46
|
+
# contains calls to methods which define the caches and give options
|
47
|
+
# for each cache.
|
48
|
+
#
|
49
|
+
# Don't instantiate directly. Rather instantiate the Instance- or
|
50
|
+
# ClassCacheConfiguration.
|
51
|
+
Configuration = Struct.new(:klass, :global_opts) do
|
52
|
+
|
53
|
+
def initialize(klass, global_opts={})
|
54
|
+
self.global_opts = global_opts
|
55
|
+
self.klass = klass
|
56
|
+
end
|
57
|
+
|
58
|
+
def method_missing(key, *args, &block)
|
59
|
+
opts = global_opts.merge(args.empty? ? {} : args.first)
|
60
|
+
case self
|
61
|
+
when InstanceCacheConfiguration
|
62
|
+
AridCache.store.add_instance_cache_configuration(klass, key, opts, block)
|
63
|
+
else
|
64
|
+
AridCache.store.add_class_cache_configuration(klass, key, opts, block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
class InstanceCacheConfiguration < Configuration; end #:nodoc:
|
69
|
+
class ClassCacheConfiguration < Configuration; end #:nodoc:
|
70
|
+
|
71
|
+
def has?(object, key)
|
72
|
+
self.include?(object_store_key(object, key))
|
73
|
+
end
|
74
|
+
|
75
|
+
# Empty the proc store
|
76
|
+
def delete!
|
77
|
+
delete_if { true }
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.instance
|
81
|
+
@@singleton_instance ||= self.new
|
82
|
+
end
|
83
|
+
|
84
|
+
def find(object, key)
|
85
|
+
self[object_store_key(object, key)]
|
86
|
+
end
|
87
|
+
|
88
|
+
def add_instance_cache_configuration(klass, key, opts, proc)
|
89
|
+
add_generic_cache_configuration(instance_store_key(klass, key), klass, key, opts, proc)
|
90
|
+
end
|
91
|
+
|
92
|
+
def add_class_cache_configuration(klass, key, opts, proc)
|
93
|
+
add_generic_cache_configuration(class_store_key(klass, key), klass, key, opts, proc)
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_object_cache_configuration(object, key, opts, proc)
|
97
|
+
add_generic_cache_configuration(object_store_key(object, key), object, key, opts, proc)
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_or_create(object, key)
|
101
|
+
store_key = object_store_key(object, key)
|
102
|
+
if self.include?(store_key)
|
103
|
+
self[store_key]
|
104
|
+
else
|
105
|
+
self[store_key] = AridCache::Store::Blueprint.new(object, key)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def add_generic_cache_configuration(store_key, *args)
|
112
|
+
AridCache.send(:method_for_cached, args[0], args[1].to_s) # TODO Right place for this?
|
113
|
+
self[store_key] = AridCache::Store::Blueprint.new(*args)
|
114
|
+
end
|
115
|
+
|
116
|
+
def initialize
|
117
|
+
end
|
118
|
+
|
119
|
+
def class_store_key(klass, key); klass.name.downcase + '-' + key.to_s; end
|
120
|
+
def instance_store_key(klass, key); klass.name.downcase.pluralize + '-' + key.to_s; end
|
121
|
+
def object_store_key(object, key)
|
122
|
+
case object; when Class; class_store_key(object, key); else; instance_store_key(object.class, key); end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
data/lib/arid_cache.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
dir = File.dirname(__FILE__)
|
2
|
+
$LOAD_PATH.unshift dir unless $LOAD_PATH.include?(dir)
|
3
|
+
|
4
|
+
require 'arid_cache/helpers'
|
5
|
+
require 'arid_cache/store'
|
6
|
+
require 'arid_cache/active_record'
|
7
|
+
require 'arid_cache/cache_proxy'
|
8
|
+
|
9
|
+
module AridCache
|
10
|
+
extend AridCache::Helpers
|
11
|
+
class Error < StandardError; end #:nodoc:
|
12
|
+
|
13
|
+
def self.cache
|
14
|
+
AridCache::CacheProxy
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.clear_caches
|
18
|
+
AridCache::CacheProxy.clear_caches
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.clear_class_caches(object)
|
22
|
+
AridCache::CacheProxy.clear_class_caches(object)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.clear_instance_caches(object)
|
26
|
+
AridCache::CacheProxy.clear_instance_caches(object)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.store
|
30
|
+
AridCache::Store.instance
|
31
|
+
end
|
32
|
+
|
33
|
+
# The old method of including this module, if you don't want to
|
34
|
+
# extend active record. Just add 'include AridCache' to your
|
35
|
+
# model class.
|
36
|
+
def self.included(base)
|
37
|
+
base.send(:include, AridCache::ActiveRecord)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Initializes AridCache for Rails.
|
41
|
+
#
|
42
|
+
# This method is called by `init.rb`,
|
43
|
+
# which is run by Rails on startup.
|
44
|
+
def self.init_rails
|
45
|
+
::ActiveRecord::Base.send(:include, AridCache::ActiveRecord)
|
46
|
+
end
|
47
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Kernel.load File.join(File.dirname(__FILE__), '..', 'init.rb')
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AridCache do
|
4
|
+
describe 'results' do
|
5
|
+
before :each do
|
6
|
+
Company.destroy_all
|
7
|
+
@company1 = Company.make(:name => 'a')
|
8
|
+
@company2 = Company.make(:name => 'b')
|
9
|
+
Company.class_caches do
|
10
|
+
ordered_by_name { Company.all(:order => 'name ASC') }
|
11
|
+
end
|
12
|
+
Company.clear_caches
|
13
|
+
end
|
14
|
+
|
15
|
+
it "order should match the original order" do
|
16
|
+
3.times do |t|
|
17
|
+
results = Company.cached_ordered_by_name
|
18
|
+
results.size.should == 2
|
19
|
+
results[0].name.should == @company1.name
|
20
|
+
results[1].name.should == @company2.name
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "order should match the order option" do
|
25
|
+
3.times do |t|
|
26
|
+
results = Company.cached_ordered_by_name(:order => 'name DESC')
|
27
|
+
results.size.should == 2
|
28
|
+
results[0].name.should == @company2.name
|
29
|
+
results[1].name.should == @company1.name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "with order option should go to the database to order" do
|
34
|
+
lambda {
|
35
|
+
Company.cached_ordered_by_name(:order => 'name DESC')
|
36
|
+
}.should query(2)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AridCache::CacheProxy::Result do
|
4
|
+
before :each do
|
5
|
+
class X; end
|
6
|
+
@result = AridCache::CacheProxy::Result.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should set the klass from a class" do
|
10
|
+
@result.klass = X
|
11
|
+
@result.klass.should be(X)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set the klass from an object" do
|
15
|
+
@result.klass = X.new
|
16
|
+
@result.klass.should be(X)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should store the klass as a string" do
|
20
|
+
@result.klass = X
|
21
|
+
@result[:klass].should == X.name
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should not have ids if it's nil" do
|
25
|
+
@result.ids = nil
|
26
|
+
@result.has_ids?.should be_false
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should have ids" do
|
30
|
+
@result.ids = [1,2,3]
|
31
|
+
@result.has_ids?.should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should have ids even if the array is empty" do
|
35
|
+
@result.ids = []
|
36
|
+
@result.has_ids?.should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should not have a count if it's nil" do
|
40
|
+
@result.count = nil
|
41
|
+
@result.has_count?.should be_false
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should have a count" do
|
45
|
+
@result.count = 3
|
46
|
+
@result.has_count?.should be_true
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should have a count even if it is zero" do
|
50
|
+
@result.count = 0
|
51
|
+
@result.has_count?.should be_true
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AridCache::CacheProxy do
|
4
|
+
describe 'preserve_order' do
|
5
|
+
before :each do
|
6
|
+
@proxy = AridCache::CacheProxy.new(Company, 'dummy-key', {})
|
7
|
+
end
|
8
|
+
|
9
|
+
it "id column should be prefixed by the table name" do
|
10
|
+
::ActiveRecord::Base.stubs(:is_mysql_adapter?).returns(true)
|
11
|
+
@proxy.send(:preserve_order, [1,2,3]).should =~ %r[#{Company.table_name}]
|
12
|
+
end
|
13
|
+
|
14
|
+
it "id column should be prefixed by the table name" do
|
15
|
+
::ActiveRecord::Base.stubs(:is_mysql_adapter?).returns(false)
|
16
|
+
@proxy.send(:preserve_order, [1,2,3]).should =~ %r[#{Company.table_name}]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe "with raw => true" do
|
21
|
+
before :each do
|
22
|
+
@user = User.make
|
23
|
+
@user.companies << Company.make
|
24
|
+
@user.companies << Company.make
|
25
|
+
@user.clear_instance_caches
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should return a CacheProxy::Result" do
|
29
|
+
@user.cached_companies(:raw => true).should be_a(AridCache::CacheProxy::Result)
|
30
|
+
end
|
31
|
+
|
32
|
+
it "result should have the same ids as the normal result" do
|
33
|
+
@user.cached_companies(:raw => true).ids.should == @user.cached_companies.collect(&:id)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should ignore :raw => false" do
|
37
|
+
@user.cached_companies(:raw => false).should == @user.cached_companies
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should only query once to seed the cache, ignoring all other options" do
|
41
|
+
lambda { @user.cached_companies(:raw => true, :limit => 0, :order => 'nonexistent_column desc') }.should query(1)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should ignore all other options if the cache has already been seeded" do
|
45
|
+
lambda {
|
46
|
+
companies = @user.cached_companies
|
47
|
+
@user.cached_companies(:raw => true, :limit => 0, :order => 'nonexistent_column').ids.should == companies.collect(&:id)
|
48
|
+
}.should query(1)
|
49
|
+
end
|
50
|
+
|
51
|
+
it "should not use the raw option when reading from the cache" do
|
52
|
+
Rails.cache.expects(:read).with(@user.arid_cache_key(:companies), {})
|
53
|
+
@user.cached_companies(:raw => true)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should work for calls to a method that ends with _count" do
|
57
|
+
@user.cached_bogus_count do
|
58
|
+
10
|
59
|
+
end
|
60
|
+
@user.cached_bogus_count(:raw => true).should == 10
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should work for calls to a method that ends with _count" do
|
64
|
+
@user.cached_companies_count(:raw => true).should == @user.cached_companies_count
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "with clear => true" do
|
69
|
+
before :each do
|
70
|
+
@user = User.make
|
71
|
+
@user.companies << Company.make
|
72
|
+
@user.companies << Company.make
|
73
|
+
@user.clear_instance_caches rescue Rails.cache.clear
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not fail if there is no cached value" do
|
77
|
+
lambda { @user.cached_companies(:clear => true) }.should_not raise_exception
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should clear the cached entry" do
|
81
|
+
key = @user.arid_cache_key(:companies)
|
82
|
+
@user.cached_companies
|
83
|
+
Rails.cache.read(key).should_not be_nil
|
84
|
+
@user.cached_companies(:clear => true)
|
85
|
+
Rails.cache.read(key).should be_nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should not read from the cache or database" do
|
89
|
+
Rails.cache.expects(:read).never
|
90
|
+
lambda {
|
91
|
+
@user.cached_companies(:clear => true)
|
92
|
+
}.should query(0)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|