ninjudd-method_cache 0.6.2

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ ---
2
+ :patch: 2
3
+ :major: 0
4
+ :minor: 6
@@ -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
@@ -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
@@ -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
+