ninjudd-method_cache 0.6.2
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.rdoc +42 -0
- data/VERSION.yml +4 -0
- data/lib/method_cache/proxy.rb +172 -0
- data/lib/method_cache.rb +162 -0
- data/test/method_cache_test.rb +114 -0
- data/test/test_helper.rb +13 -0
- metadata +61 -0
data/README.rdoc
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
= MethodCache
|
2
|
+
|
3
|
+
MethodCache lets you easily cache the results of any instance method or class method in
|
4
|
+
Ruby.
|
5
|
+
|
6
|
+
== Usage:
|
7
|
+
|
8
|
+
class Foo
|
9
|
+
extend MethodCache
|
10
|
+
|
11
|
+
cache_method :bar
|
12
|
+
def bar
|
13
|
+
# do expensive calculation
|
14
|
+
end
|
15
|
+
|
16
|
+
cache_class_method :baz, :clone => true, :expiry => 1.day
|
17
|
+
def self.baz
|
18
|
+
# do some expensive calculation that will be invalid tomorrow
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
foo = Foo.new
|
23
|
+
foo.bar # does calculation
|
24
|
+
foo.bar # cached
|
25
|
+
|
26
|
+
Foo.baz # does calculation
|
27
|
+
Foo.baz # cached
|
28
|
+
|
29
|
+
Foo.invalidate_cached_method(:baz)
|
30
|
+
|
31
|
+
Foo.baz # does calculation
|
32
|
+
Foo.baz # cached
|
33
|
+
|
34
|
+
== Install:
|
35
|
+
|
36
|
+
sudo gem install ninjudd-memcache -s http://gems.github.com
|
37
|
+
sudo gem install ninjudd-cache_version -s http://gems.github.com
|
38
|
+
sudo gem install ninjudd-method_cache -s http://gems.github.com
|
39
|
+
|
40
|
+
== License:
|
41
|
+
|
42
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see LICENSE
|
data/VERSION.yml
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
module MethodCache
|
2
|
+
class Proxy
|
3
|
+
attr_reader :method_name, :opts, :args, :target
|
4
|
+
|
5
|
+
def initialize(method_name, opts)
|
6
|
+
@method_name = method_name
|
7
|
+
@opts = opts
|
8
|
+
end
|
9
|
+
|
10
|
+
def bind(target, args)
|
11
|
+
self.clone.bind!(target, args)
|
12
|
+
end
|
13
|
+
|
14
|
+
def bind!(target, args)
|
15
|
+
@target = target
|
16
|
+
@args = args
|
17
|
+
@key = nil
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def invalidate
|
22
|
+
if block_given?
|
23
|
+
# Only invalidate if the block returns true.
|
24
|
+
value = cache[key]
|
25
|
+
return if value and not yield(value)
|
26
|
+
end
|
27
|
+
cache.delete(key)
|
28
|
+
end
|
29
|
+
|
30
|
+
def context
|
31
|
+
opts[:context]
|
32
|
+
end
|
33
|
+
|
34
|
+
def version
|
35
|
+
dynamic_opt(:version)
|
36
|
+
end
|
37
|
+
|
38
|
+
def cached?
|
39
|
+
not cache[key].nil?
|
40
|
+
end
|
41
|
+
|
42
|
+
def update
|
43
|
+
value = block_given? ? yield(cache[key]) : target.send(method_name_without_caching, *args)
|
44
|
+
write_to_cache(key, value)
|
45
|
+
value
|
46
|
+
end
|
47
|
+
|
48
|
+
def value
|
49
|
+
value = cache[key]
|
50
|
+
value = nil unless valid?(:load, value)
|
51
|
+
|
52
|
+
if value.nil?
|
53
|
+
value = target.send(method_name_without_caching, *args)
|
54
|
+
write_to_cache(key, value) if valid?(:save, value)
|
55
|
+
end
|
56
|
+
|
57
|
+
value = nil if value == NULL
|
58
|
+
if clone? and value
|
59
|
+
value.clone
|
60
|
+
else
|
61
|
+
value
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
NULL = 'NULL'
|
66
|
+
def method_with_caching
|
67
|
+
proxy = self # Need access to the proxy in the closure.
|
68
|
+
|
69
|
+
lambda do |*args|
|
70
|
+
proxy.bind(self, args).value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def method_name_without_caching
|
75
|
+
@method_name_without_caching ||= begin
|
76
|
+
base_name, punctuation = method_name.to_s.sub(/([?!=])$/, ''), $1
|
77
|
+
"#{base_name}_without_caching#{punctuation}"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def cache
|
82
|
+
if @cache.nil?
|
83
|
+
@cache = opts[:cache] || MethodCache.default_cache
|
84
|
+
@cache = MemCache.pool[@cache] if @cache.kind_of?(Symbol)
|
85
|
+
end
|
86
|
+
@cache
|
87
|
+
end
|
88
|
+
|
89
|
+
def local?
|
90
|
+
cache.kind_of?(Hash)
|
91
|
+
end
|
92
|
+
|
93
|
+
def clone?
|
94
|
+
!!opts[:clone]
|
95
|
+
end
|
96
|
+
|
97
|
+
def key
|
98
|
+
if @key.nil?
|
99
|
+
arg_string = ([method_name, target] + args).collect do |arg|
|
100
|
+
object_key(arg)
|
101
|
+
end.join('|')
|
102
|
+
@key = ['m', version, arg_string].compact.join('|')
|
103
|
+
end
|
104
|
+
@key
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def expiry(value)
|
110
|
+
dynamic_opt(:expiry, value).to_i
|
111
|
+
end
|
112
|
+
|
113
|
+
def valid?(type, value)
|
114
|
+
name = "#{type}_validation".to_sym
|
115
|
+
return true unless opts[name]
|
116
|
+
return unless value
|
117
|
+
|
118
|
+
dynamic_opt(name, value)
|
119
|
+
end
|
120
|
+
|
121
|
+
def dynamic_opt(name, value = nil)
|
122
|
+
if opts[name].kind_of?(Proc)
|
123
|
+
proc = opts[name].bind(target)
|
124
|
+
case proc.arity
|
125
|
+
when 0: proc.call()
|
126
|
+
when 1: proc.call(value)
|
127
|
+
else
|
128
|
+
proc.call(value, *args)
|
129
|
+
end
|
130
|
+
else
|
131
|
+
opts[name]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def write_to_cache(key, value)
|
136
|
+
if cache.kind_of?(Hash)
|
137
|
+
raise 'expiry not permitted when cache is a Hash' if opts[:expiry]
|
138
|
+
cache[key] = value
|
139
|
+
else
|
140
|
+
value = value.nil? ? NULL : value
|
141
|
+
cache.set(key, value, expiry(value))
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def object_key(arg)
|
146
|
+
return "#{class_key(arg.class)}-#{arg.string_hash}" if arg.respond_to?(:string_hash)
|
147
|
+
|
148
|
+
case arg
|
149
|
+
when NilClass : 'nil'
|
150
|
+
when TrueClass : 'true'
|
151
|
+
when FalseClass : 'false'
|
152
|
+
when Numeric : arg.to_s
|
153
|
+
when Symbol : ":#{arg}"
|
154
|
+
when String : "'#{arg}'"
|
155
|
+
when Class, Module : class_key(arg)
|
156
|
+
when Hash
|
157
|
+
'{' + arg.collect {|key, value| "#{object_key(key)}=#{object_key(value)}"}.sort.join(',') + '}'
|
158
|
+
when Array
|
159
|
+
'[' + arg.collect {|item| object_key(item)}.join(',') + ']'
|
160
|
+
when defined?(ActiveRecord::Base) && ActiveRecord::Base
|
161
|
+
"#{class_key(arg.class)}-#{arg.id}"
|
162
|
+
else
|
163
|
+
hash = local? ? arg.hash : Marshal.dump(arg).hash
|
164
|
+
"#{class_key(arg.class)}-#{hash}"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def class_key(klass)
|
169
|
+
klass.respond_to?(:version) ? "#{klass.name}_#{klass.version(context)}" : klass.name
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
data/lib/method_cache.rb
ADDED
@@ -0,0 +1,162 @@
|
|
1
|
+
require 'memcache'
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__))
|
4
|
+
require 'method_cache/proxy'
|
5
|
+
|
6
|
+
module MethodCache
|
7
|
+
def cache_method(method_name, opts = {})
|
8
|
+
method_name = method_name.to_sym
|
9
|
+
proxy = opts.kind_of?(Proxy) ? opts : Proxy.new(method_name, opts)
|
10
|
+
|
11
|
+
if self.class == Class
|
12
|
+
return if instance_methods.include?(proxy.method_name_without_caching)
|
13
|
+
|
14
|
+
if cached_instance_methods.empty?
|
15
|
+
include(InvalidationMethods)
|
16
|
+
extend(MethodAdded)
|
17
|
+
end
|
18
|
+
|
19
|
+
cached_instance_methods[method_name] = nil
|
20
|
+
begin
|
21
|
+
# Replace instance method.
|
22
|
+
alias_method proxy.method_name_without_caching, method_name
|
23
|
+
define_method method_name, proxy.method_with_caching
|
24
|
+
rescue NameError => e
|
25
|
+
# The method has not been defined yet. We will alias it in method_added.
|
26
|
+
# pp e, e.backtrace
|
27
|
+
end
|
28
|
+
cached_instance_methods[method_name] = proxy
|
29
|
+
|
30
|
+
elsif self.class == Module
|
31
|
+
# We will alias all methods when the module is mixed-in.
|
32
|
+
extend(ModuleAdded) if cached_module_methods.empty?
|
33
|
+
cached_module_methods[method_name.to_sym] = proxy
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def cache_class_method(method_name, opts = {})
|
38
|
+
method_name = method_name.to_sym
|
39
|
+
proxy = opts.kind_of?(Proxy) ? opts : Proxy.new(method_name, opts)
|
40
|
+
|
41
|
+
return if methods.include?(proxy.method_name_without_caching)
|
42
|
+
|
43
|
+
if cached_class_methods.empty?
|
44
|
+
extend(InvalidationMethods)
|
45
|
+
extend(SingletonMethodAdded)
|
46
|
+
end
|
47
|
+
|
48
|
+
method_name = method_name.to_sym
|
49
|
+
cached_class_methods[method_name] = nil
|
50
|
+
begin
|
51
|
+
# Replace class method.
|
52
|
+
(class << self; self; end).module_eval do
|
53
|
+
alias_method proxy.method_name_without_caching, method_name
|
54
|
+
define_method method_name, proxy.method_with_caching
|
55
|
+
end
|
56
|
+
rescue NameError => e
|
57
|
+
# The method has not been defined yet. We will alias it in singleton_method_added.
|
58
|
+
# pp e, e.backtrace
|
59
|
+
end
|
60
|
+
cached_class_methods[method_name] = proxy
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.default_cache
|
64
|
+
@default_cache ||= {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def cached_instance_methods(method_name = nil)
|
68
|
+
if method_name
|
69
|
+
method_name = method_name.to_sym
|
70
|
+
ancestors.each do |klass|
|
71
|
+
next unless klass.kind_of?(MethodCache)
|
72
|
+
proxy = klass.cached_instance_methods[method_name]
|
73
|
+
return proxy if proxy
|
74
|
+
end
|
75
|
+
nil
|
76
|
+
else
|
77
|
+
@cached_instance_methods ||= {}
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def cached_class_methods(method_name = nil)
|
82
|
+
if method_name
|
83
|
+
method_name = method_name.to_sym
|
84
|
+
ancestors.each do |klass|
|
85
|
+
next unless klass.kind_of?(MethodCache)
|
86
|
+
proxy = klass.cached_class_methods[method_name]
|
87
|
+
return proxy if proxy
|
88
|
+
end
|
89
|
+
nil
|
90
|
+
else
|
91
|
+
@cached_class_methods ||= {}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def cached_module_methods(method_name = nil)
|
96
|
+
if method_name
|
97
|
+
cached_module_methods[method_name.to_sym]
|
98
|
+
else
|
99
|
+
@cached_module_methods ||= {}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
module InvalidationMethods
|
104
|
+
def invalidate_cached_method(method_name, *args, &block)
|
105
|
+
cached_method(method_name, args).invalidate(&block)
|
106
|
+
end
|
107
|
+
|
108
|
+
def method_value_cached?(method_name, *args)
|
109
|
+
cached_method(method_name, args).cached?
|
110
|
+
end
|
111
|
+
|
112
|
+
def update_cached_method(method_name, *args, &block)
|
113
|
+
cached_method(method_name, args).update(&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def cached_method(method_name, args)
|
119
|
+
if self.kind_of?(Class) or self.kind_of?(Module)
|
120
|
+
proxy = cached_class_methods(method_name)
|
121
|
+
else
|
122
|
+
proxy = self.class.send(:cached_instance_methods, method_name)
|
123
|
+
end
|
124
|
+
raise "method '#{method_name}' not cached" unless proxy
|
125
|
+
proxy.bind(self, args)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
module MethodAdded
|
130
|
+
def method_added(method_name)
|
131
|
+
if proxy = cached_instance_methods(method_name)
|
132
|
+
cache_method(method_name, proxy)
|
133
|
+
end
|
134
|
+
super
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
module SingletonMethodAdded
|
139
|
+
def singleton_method_added(method_name)
|
140
|
+
if proxy = cached_class_methods(method_name)
|
141
|
+
cache_class_method(method_name, proxy)
|
142
|
+
end
|
143
|
+
super
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
module ModuleAdded
|
148
|
+
def extended(mod)
|
149
|
+
mod.extend(MethodCache)
|
150
|
+
cached_module_methods.each do |method_name, proxy|
|
151
|
+
mod.cache_class_method(method_name, proxy)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def included(mod)
|
156
|
+
mod.extend(MethodCache)
|
157
|
+
cached_module_methods.each do |method_name, proxy|
|
158
|
+
mod.cache_method(method_name, proxy)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
class Foo
|
4
|
+
extend MethodCache
|
5
|
+
|
6
|
+
def foo(i)
|
7
|
+
@i ||= 0
|
8
|
+
@i += i
|
9
|
+
end
|
10
|
+
cache_method :foo
|
11
|
+
|
12
|
+
cache_method :bar
|
13
|
+
def bar
|
14
|
+
@i ||= 0
|
15
|
+
@i += 1
|
16
|
+
end
|
17
|
+
|
18
|
+
@@i = 0
|
19
|
+
def baz(i)
|
20
|
+
@@i += i
|
21
|
+
end
|
22
|
+
cache_method :baz, :cache => :remote
|
23
|
+
end
|
24
|
+
|
25
|
+
module Bar
|
26
|
+
extend MethodCache
|
27
|
+
|
28
|
+
cache_method :foo
|
29
|
+
def foo(i)
|
30
|
+
@i ||= 0
|
31
|
+
@i += i
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Baz
|
36
|
+
include Bar
|
37
|
+
end
|
38
|
+
|
39
|
+
class TestMethodCache < Test::Unit::TestCase
|
40
|
+
should 'cache methods locally' do
|
41
|
+
a = Foo.new
|
42
|
+
f1 = a.foo(1)
|
43
|
+
f2 = a.foo(2)
|
44
|
+
|
45
|
+
assert_equal 1, f1
|
46
|
+
assert_equal 3, f2
|
47
|
+
|
48
|
+
assert f1 == a.foo(1)
|
49
|
+
assert f1 != f2
|
50
|
+
assert f2 == a.foo(2)
|
51
|
+
|
52
|
+
b = a.bar
|
53
|
+
assert b == a.bar
|
54
|
+
assert b == a.bar
|
55
|
+
end
|
56
|
+
|
57
|
+
should 'cache methods remotely' do
|
58
|
+
a = Foo.new
|
59
|
+
b1 = a.baz(1)
|
60
|
+
b2 = a.baz(2)
|
61
|
+
|
62
|
+
assert_equal 1, b1
|
63
|
+
assert_equal 3, b2
|
64
|
+
|
65
|
+
assert b1 == a.baz(1)
|
66
|
+
assert b1 != b2
|
67
|
+
assert b2 == a.baz(2)
|
68
|
+
end
|
69
|
+
|
70
|
+
should 'cache methods for mixins' do
|
71
|
+
a = Baz.new
|
72
|
+
|
73
|
+
assert_equal 1, a.foo(1)
|
74
|
+
assert_equal 1, a.foo(1)
|
75
|
+
assert_equal 3, a.foo(2)
|
76
|
+
assert_equal 3, a.foo(2)
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'invalidate cached method' do
|
80
|
+
a = Foo.new
|
81
|
+
|
82
|
+
assert_equal 1, a.foo(1)
|
83
|
+
assert_equal 3, a.foo(2)
|
84
|
+
|
85
|
+
a.invalidate_cached_method(:foo, 1)
|
86
|
+
|
87
|
+
assert_equal 4, a.foo(1)
|
88
|
+
assert_equal 3, a.foo(2)
|
89
|
+
end
|
90
|
+
|
91
|
+
should 'use consistent local keys' do
|
92
|
+
a = Foo.new
|
93
|
+
o = Object.new
|
94
|
+
a_hash = a.hash
|
95
|
+
o_hash = o.hash
|
96
|
+
|
97
|
+
5.times do
|
98
|
+
key = a.send(:cached_method, :bar, [{'a' => 3, 'b' => [5,6], 'c' => o}, [1,nil,{:o => o}]]).key
|
99
|
+
assert_equal "m|:bar|Foo-#{a_hash}|{'a'=3,'b'=[5,6],'c'=Object-#{o_hash}}|[1,nil,{:o=Object-#{o_hash}}]", key
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
should 'use consistent remote keys' do
|
104
|
+
a = Foo.new
|
105
|
+
o = Object.new
|
106
|
+
a_hash = Marshal.dump(a).hash
|
107
|
+
o_hash = Marshal.dump(o).hash
|
108
|
+
|
109
|
+
5.times do
|
110
|
+
key = a.send(:cached_method, :baz, [{:a => 3, :b => [5,6], :c => o}, [false,true,{:o => o}]]).key
|
111
|
+
assert_equal "m|:baz|Foo-#{a_hash}|{:a=3,:b=[5,6],:c=Object-#{o_hash}}|[false,true,{:o=Object-#{o_hash}}]", key
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'test/unit'
|
3
|
+
require 'shoulda'
|
4
|
+
require 'mocha'
|
5
|
+
require 'pp'
|
6
|
+
|
7
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
|
8
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../../memcache/lib"
|
9
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + "/../../cache_version/lib"
|
10
|
+
require 'method_cache'
|
11
|
+
|
12
|
+
class Test::Unit::TestCase
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ninjudd-method_cache
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.6.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Balthrop
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-07-29 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Simple memcache-based memoization library for Ruby
|
17
|
+
email: code@justinbalthrop.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files: []
|
23
|
+
|
24
|
+
files:
|
25
|
+
- README.rdoc
|
26
|
+
- VERSION.yml
|
27
|
+
- lib/method_cache
|
28
|
+
- lib/method_cache/proxy.rb
|
29
|
+
- lib/method_cache.rb
|
30
|
+
- test/method_cache_test.rb
|
31
|
+
- test/test_helper.rb
|
32
|
+
has_rdoc: true
|
33
|
+
homepage: http://github.com/ninjudd/method_cache
|
34
|
+
licenses:
|
35
|
+
post_install_message:
|
36
|
+
rdoc_options:
|
37
|
+
- --inline-source
|
38
|
+
- --charset=UTF-8
|
39
|
+
require_paths:
|
40
|
+
- lib
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
requirements:
|
43
|
+
- - ">="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: "0"
|
46
|
+
version:
|
47
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: "0"
|
52
|
+
version:
|
53
|
+
requirements: []
|
54
|
+
|
55
|
+
rubyforge_project:
|
56
|
+
rubygems_version: 1.3.5
|
57
|
+
signing_key:
|
58
|
+
specification_version: 2
|
59
|
+
summary: Simple memcache-based memoization library for Ruby
|
60
|
+
test_files: []
|
61
|
+
|