cached_attribute 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,43 @@
1
+ CachedAttribute
2
+ ===============
3
+
4
+ CachedAttribute is a plugin that allows expensive model attributes to be cached on a
5
+ per-instance basis. It supports memoization, multiple cache stores, invalidation, and ttl.
6
+
7
+ It can also be used to cached arbitrary methods based on their
8
+
9
+ Example
10
+ =======
11
+
12
+ class ComplicatedModel < ActiveRecord::Base
13
+
14
+ def expensive_operation
15
+ ... something expensive ...
16
+ end
17
+
18
+ # caches the calls to expensive_operation
19
+ cached_attribute :expensive_operation
20
+
21
+ class << self
22
+ def expensive_class_method(param1)
23
+ ... something expensive ...
24
+ end
25
+ cached_attribute :expensive_class_method
26
+ end
27
+
28
+ end
29
+
30
+ c = ComplicatedModel.new
31
+ c.expensive_operation # => slow
32
+ c.expensive_operation # => fast!
33
+
34
+ c.invalidate_expensive_operation
35
+ c.expensive_operation # => slow
36
+
37
+ ComplicatedModel.expensive_class_method('some string or something') # => slow
38
+ ComplicatedModel.expensive_class_method('some other string or something') # => slow
39
+ ComplicatedModel.expensive_class_method('some string or something') # => fast
40
+
41
+
42
+
43
+ Copyright (c) 2009 Avvo, Inc., released under the MIT license
@@ -0,0 +1,106 @@
1
+ require 'digest/sha1'
2
+ require 'pp'
3
+
4
+ module CachedAttribute
5
+ module ClassMethods
6
+
7
+ #
8
+ # specifies that the attribute +attr+ should be cached. By default, the cache specified
9
+ # by the instance variable +Cache+ is used. +cached_attribute+ will also memoize the
10
+ # attribute by default.
11
+ #
12
+ # Options:
13
+ #
14
+ # :ttl - The ttl (in seconds) for the cached attribute. Defaults to 5 minutes.
15
+ # :cache - The cache store object to use for caching. Must respond to #get (with block)
16
+ # and #delete and #set or #put (key, value, expiry).
17
+ # :identifier - A block that takes a single parameter (of the object containing the cached
18
+ # attribute) and returns an identifier unique to that object. Defaults to
19
+ # lambda {|s| s.id}, which works fine for activerecord objects.
20
+ # :memoize - Whether the return value should be memoized into an instance variable.
21
+ # Defaults to true.
22
+ #
23
+ #
24
+ # Example:
25
+ #
26
+ # class ComplicatedModel < ActiveRecord::Base
27
+ #
28
+ # def expensive_operation
29
+ # ... something expensive ...
30
+ # end
31
+ #
32
+ # # caches the calls to expensive_operation
33
+ # cached_attribute :expensive_operation
34
+ #
35
+ # end
36
+ #
37
+ def cached_attribute(attr, opts = {})
38
+
39
+ cache = opts[:cache] || (defined?(CACHE) && CACHE)
40
+ ttl = opts[:ttl] || 300
41
+
42
+ define_method("#{attr}_cache_key") do |*prms|
43
+ if prms.present?
44
+ identifier = prms.pretty_inspect # pretty inspect works nicely because it properly escpaes all the params into one string
45
+ else
46
+ identifier = opts[:identifier] ? opts[:identifier].call(self).to_s : self.id.to_s
47
+ end
48
+ # need to special case when we're caching class methods
49
+ klass_string = "#{self.class == Class ? self.name + '::self' : self.class.name}"
50
+ "#{klass_string}::#{attr}::#{Digest::SHA1.hexdigest(identifier)}"
51
+ end
52
+
53
+ define_method("#{attr}_with_caching") do |*prms|
54
+ if cache && (instance_variable_get("@#{attr}").nil? || prms.present?)
55
+ cache.fetch(send("#{attr}_cache_key", *prms), ttl) do
56
+ send("#{attr}_without_caching", *prms)
57
+ end
58
+ else
59
+ send("#{attr}_without_caching", *prms)
60
+ end
61
+ end
62
+
63
+ alias_method "#{attr}_without_caching", attr
64
+ alias_method attr, "#{attr}_with_caching"
65
+
66
+ unless opts[:memoize] == false
67
+
68
+ define_method("#{attr}_with_memoization") do |*prms|
69
+ value = instance_variable_get("@#{attr}")
70
+ if value.nil? || prms.present? # skip memoization if there are prms
71
+ value = instance_variable_set("@#{attr}", send("#{attr}_without_memoization", *prms))
72
+ end
73
+ value
74
+ end
75
+
76
+ alias_method "#{attr}_without_memoization", attr
77
+ alias_method attr, "#{attr}_with_memoization"
78
+ end
79
+
80
+ define_method("invalidate_#{attr}") do |*prms|
81
+ if cache
82
+ cache_key = send("#{attr}_cache_key", *prms)
83
+ cache.delete(cache_key)
84
+ end
85
+ end
86
+
87
+ # refreshs the cached attribute
88
+ define_method("refresh_#{attr}") do |*prms|
89
+ if cache
90
+ set_method = cache.respond_to?(:set) ? :set : :put
91
+ val = send("#{attr}_without_caching")
92
+ cache.send(set_method, send("#{attr}_cache_key", *prms), val, ttl)
93
+ end
94
+ end
95
+
96
+ end
97
+ end
98
+
99
+ def self.included(klass)
100
+ klass.extend ClassMethods
101
+ end
102
+ end
103
+
104
+ Object.instance_eval do
105
+ include CachedAttribute
106
+ end
@@ -0,0 +1,170 @@
1
+ require 'test/unit'
2
+ require File.join(File.dirname(__FILE__), '../lib/cached_attribute')
3
+ require 'init'
4
+ require 'rubygems'
5
+ require 'activesupport'
6
+
7
+ class DummyCache
8
+
9
+ def self.reset
10
+ @data = {}
11
+ @cache_hit = false
12
+ @last_get = nil
13
+ @last_set = nil
14
+ end
15
+ reset
16
+
17
+ def self.cache_hit?
18
+ @cache_hit
19
+ end
20
+
21
+ def self.last_set
22
+ @last_set
23
+ end
24
+
25
+ def self.last_get
26
+ @last_get
27
+ end
28
+
29
+ def self.get(key, ttl = 60, &block)
30
+
31
+ value, expires = @data[key]
32
+
33
+ if !value || expires < Time.now
34
+ if block
35
+ value, expires = set(key, block.call, ttl)
36
+ else
37
+ value, expires = Time.now
38
+ end
39
+ else
40
+ @cache_hit = true
41
+ @last_get = value
42
+ end
43
+ value
44
+ end
45
+
46
+ def self.delete(key)
47
+ @data[key] = nil
48
+ end
49
+
50
+ def self.set(key, value, ttl = 60)
51
+ @last_set = Time.now
52
+ @data[key] = [value, Time.now + ttl]
53
+ end
54
+
55
+ end
56
+
57
+ class BaseModel
58
+
59
+ class << self
60
+ def expensive_class_method(number)
61
+ number + 50
62
+ end
63
+ cached_attribute :expensive_class_method, :cache => DummyCache
64
+ end
65
+
66
+ def expensive_instance_method(p1, p2)
67
+ p1 + p2
68
+ end
69
+ cached_attribute :expensive_instance_method, :cache => DummyCache
70
+
71
+ def expensive_call
72
+ "hello" * 50
73
+ end
74
+
75
+ cached_attribute :expensive_call, :cache => DummyCache, :identifier => lambda {|s| 1}
76
+
77
+ def no_memoization
78
+ "hello" * 50
79
+ end
80
+
81
+ cached_attribute :no_memoization, :cache => DummyCache, :identifier => lambda {|s| 2}, :memoize => false
82
+
83
+ def low_ttl
84
+ "hello" * 50
85
+ end
86
+
87
+ cached_attribute :low_ttl, :cache => DummyCache, :identifier => lambda {|s| 3}, :memoize => false, :ttl => 1
88
+ end
89
+
90
+ class CachedAttributeTest < Test::Unit::TestCase
91
+
92
+ def setup
93
+ DummyCache.reset
94
+ @m = BaseModel.new
95
+ end
96
+
97
+ def test_cached_attribute_method_put_on_class
98
+ assert BaseModel.respond_to?(:cached_attribute), "Cached attribute method was not found."
99
+ end
100
+
101
+ def test_cached_attribute_enabled
102
+ v = @m.no_memoization
103
+ assert !DummyCache.cache_hit?, "Cache should not be hit on initial call"
104
+ @m.no_memoization
105
+ assert DummyCache.cache_hit?, "Cache should be hit on the second call"
106
+ assert_equal(v, DummyCache.last_get)
107
+ end
108
+
109
+ def test_cached_attribute_memoizes_attribute
110
+ v1 = @m.expensive_call
111
+ assert_equal(v1, @m.instance_variable_get("@expensive_call"))
112
+ v2 = @m.expensive_call
113
+ assert_equal(v1, v2)
114
+ end
115
+
116
+ def test_cached_attribute_can_be_invalidated
117
+ @m.no_memoization
118
+ assert !DummyCache.cache_hit?, "Cache should not be hit on initial call"
119
+ @m.invalidate_no_memoization
120
+ @m.no_memoization
121
+ assert !DummyCache.cache_hit?, "Cache should not be hit after invalidation"
122
+ end
123
+
124
+ def test_ttl_should_work
125
+ @m.low_ttl
126
+ assert !DummyCache.cache_hit?, "Cache should not be hit on initial call"
127
+ sleep 1
128
+ @m.low_ttl
129
+ assert !DummyCache.cache_hit?, "Cache should not be hit after 1 second"
130
+ end
131
+
132
+ def test_refresh
133
+ @m.no_memoization
134
+ assert !DummyCache.cache_hit?, "Cache should not be hit on initial call"
135
+ last_set = DummyCache.last_set
136
+ @m.no_memoization
137
+ assert DummyCache.cache_hit?, "Cache should be a hit"
138
+ sleep 1
139
+ @m.refresh_no_memoization
140
+ next_set = DummyCache.last_set
141
+ @m.no_memoization
142
+ assert DummyCache.cache_hit?, "Cache should still be a hit"
143
+ assert(last_set < next_set)
144
+ end
145
+
146
+ def test_on_class_method_with_params
147
+ assert_equal(51, BaseModel.expensive_class_method(1))
148
+ assert !DummyCache.cache_hit?, 'First call should be miss'
149
+
150
+ assert_equal(51, BaseModel.expensive_class_method(1))
151
+ assert DummyCache.cache_hit?, 'Second call should be hit'
152
+ end
153
+
154
+ def test_on_instance_method_with_params
155
+ assert_equal(11, @m.expensive_instance_method(5, 6))
156
+ assert !DummyCache.cache_hit?, 'First call should be miss'
157
+
158
+ assert_equal(11, @m.expensive_instance_method(5, 6))
159
+ assert DummyCache.cache_hit?, 'Second call should be hit'
160
+ end
161
+
162
+ def test_class_method_key
163
+ assert_equal("BaseModel::self::expensive_class_method::#{Digest::SHA1.hexdigest([1].pretty_inspect)}", BaseModel.expensive_class_method_cache_key(1))
164
+ end
165
+
166
+ def test_instance_method_key
167
+ assert_equal("BaseModel::expensive_instance_method::#{Digest::SHA1.hexdigest([1].pretty_inspect)}", @m.expensive_instance_method_cache_key(1))
168
+ end
169
+
170
+ end
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cached_attribute
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Justin Weiss
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-12 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description:
15
+ email: jweiss@avvo.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files:
19
+ - README.markdown
20
+ files:
21
+ - lib/cached_attribute.rb
22
+ - test/cached_attribute_test.rb
23
+ - README.markdown
24
+ homepage: http://code.avvo.com/2009/03/introducing-cached_attribute-cached-values-across-object-instances.html
25
+ licenses: []
26
+ post_install_message:
27
+ rdoc_options: []
28
+ require_paths:
29
+ - lib
30
+ required_ruby_version: !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ! '>='
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ required_rubygems_version: !ruby/object:Gem::Requirement
37
+ none: false
38
+ requirements:
39
+ - - ! '>='
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubyforge_project:
44
+ rubygems_version: 1.8.24
45
+ signing_key:
46
+ specification_version: 3
47
+ summary: Cache values across object instances
48
+ test_files:
49
+ - test/cached_attribute_test.rb