cached_attribute 0.0.1

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.
@@ -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