djmaze-arid_cache 1.1.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/.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
|