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.
- data/README.markdown +43 -0
- data/lib/cached_attribute.rb +106 -0
- data/test/cached_attribute_test.rb +170 -0
- metadata +49 -0
data/README.markdown
ADDED
@@ -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
|