cached_model 1.0.0 → 1.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/LICENSE +30 -0
- data/Manifest.txt +2 -0
- data/Rakefile +9 -1
- data/lib/cached_model.rb +40 -30
- data/test/test_cached_model.rb +305 -0
- metadata +4 -2
data/LICENSE
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
All original code copyright 2005 Bob Cottrell and Eric Hodel, The Robot Co-op.
|
2
|
+
All rights reserved.
|
3
|
+
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
5
|
+
modification, are permitted provided that the following conditions
|
6
|
+
are met:
|
7
|
+
|
8
|
+
1. Redistributions of source code must retain the above copyright
|
9
|
+
notice, this list of conditions and the following disclaimer.
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright
|
11
|
+
notice, this list of conditions and the following disclaimer in the
|
12
|
+
documentation and/or other materials provided with the distribution.
|
13
|
+
3. Neither the names of the authors nor the names of their contributors
|
14
|
+
may be used to endorse or promote products derived from this software
|
15
|
+
without specific prior written permission.
|
16
|
+
4. Redistribution in Rails or any sub-projects of Rails is not allowed
|
17
|
+
until Rails runs without warnings with the ``-W2'' flag enabled.
|
18
|
+
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS
|
20
|
+
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
21
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
22
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE
|
23
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
24
|
+
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
25
|
+
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
|
26
|
+
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
27
|
+
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
28
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
|
29
|
+
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
30
|
+
|
data/Manifest.txt
CHANGED
data/Rakefile
CHANGED
@@ -8,7 +8,7 @@ $VERBOSE = nil
|
|
8
8
|
|
9
9
|
spec = Gem::Specification.new do |s|
|
10
10
|
s.name = 'cached_model'
|
11
|
-
s.version = '1.0.
|
11
|
+
s.version = '1.0.1'
|
12
12
|
s.summary = 'An ActiveRecord::Base model that caches records'
|
13
13
|
s.authors = 'Robert Cottrell, Eric Hodel'
|
14
14
|
s.email = 'bob@robotcoop.com'
|
@@ -42,6 +42,14 @@ Rake::RDocTask.new :rdoc do |rd|
|
|
42
42
|
rd.options << '-d' if `which dot` =~ /\/dot/
|
43
43
|
end
|
44
44
|
|
45
|
+
desc 'Generate RDoc for dev.robotcoop.com'
|
46
|
+
Rake::RDocTask.new :dev_rdoc do |rd|
|
47
|
+
rd.rdoc_dir = '../../../www/trunk/dev/html/Libraries/cached_model'
|
48
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
49
|
+
rd.main = 'README'
|
50
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
51
|
+
end
|
52
|
+
|
45
53
|
desc 'Build Gem'
|
46
54
|
Rake::GemPackageTask.new spec do |pkg|
|
47
55
|
pkg.need_tar = true
|
data/lib/cached_model.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
$TESTING = (defined? $TESTING) ? $TESTING : false
|
2
|
+
|
3
|
+
require 'timeout'
|
4
|
+
require 'memcache_util' unless $TESTING == :cached_model
|
2
5
|
|
3
6
|
##
|
4
7
|
# An abstract ActiveRecord descendant that caches records in memcache and in
|
@@ -51,7 +54,7 @@ class CachedModel < ActiveRecord::Base
|
|
51
54
|
end
|
52
55
|
|
53
56
|
##
|
54
|
-
# Invalidate the
|
57
|
+
# Invalidate the local process cache. This should be called from a before
|
55
58
|
# filter at the beginning of each request.
|
56
59
|
|
57
60
|
def self.cache_reset
|
@@ -67,10 +70,15 @@ class CachedModel < ActiveRecord::Base
|
|
67
70
|
# Only handle simple find requests. If the request was more complicated,
|
68
71
|
# let the base class handle it, but store the retrieved records in the
|
69
72
|
# local cache in case we need them later.
|
70
|
-
if $TESTING
|
73
|
+
if ($TESTING and $TESTING != :cached_model) or
|
74
|
+
args.length != 1 or not Fixnum === args.first then
|
71
75
|
records = super
|
72
|
-
if RAILS_ENV
|
76
|
+
return records if RAILS_ENV == 'test'
|
77
|
+
case records
|
78
|
+
when Array then
|
73
79
|
records.each { |r| r.cache_store }
|
80
|
+
when CachedModel then
|
81
|
+
records.cache_store
|
74
82
|
end
|
75
83
|
return records
|
76
84
|
end
|
@@ -111,27 +119,15 @@ class CachedModel < ActiveRecord::Base
|
|
111
119
|
# key, use a simple find call instead.
|
112
120
|
|
113
121
|
def self.find_by_sql(*args)
|
114
|
-
unless @skip_find_hack or $TESTING then
|
122
|
+
unless @skip_find_hack or ($TESTING and $TESTING != :cached_model) then
|
115
123
|
if args.first =~ /SELECT \* FROM #{table_name} WHERE \(#{table_name}\.#{primary_key} = '?(\d+)'?\) LIMIT 1/ then
|
116
|
-
return [
|
124
|
+
return [find($1.to_i)]
|
117
125
|
end
|
118
126
|
end
|
119
127
|
|
120
128
|
return super
|
121
129
|
end
|
122
130
|
|
123
|
-
##
|
124
|
-
# Delete the entry from the cache so that the next call goes to the database
|
125
|
-
# for the freshest copy of the record. This will also ensure that if for
|
126
|
-
# some reason a stale copy of the record was cached we can get rid of it.
|
127
|
-
|
128
|
-
def update
|
129
|
-
cache_delete
|
130
|
-
val = super
|
131
|
-
cache_store
|
132
|
-
return val
|
133
|
-
end
|
134
|
-
|
135
131
|
##
|
136
132
|
# Delete the entry from the cache now that it isn't in the DB.
|
137
133
|
|
@@ -147,21 +143,17 @@ class CachedModel < ActiveRecord::Base
|
|
147
143
|
def reload
|
148
144
|
cache_delete
|
149
145
|
return super
|
146
|
+
ensure
|
147
|
+
cache_store
|
150
148
|
end
|
151
149
|
|
152
150
|
##
|
153
|
-
#
|
154
|
-
|
155
|
-
def cache_local
|
156
|
-
return CachedModel.cache_local
|
157
|
-
end
|
158
|
-
|
159
|
-
##
|
160
|
-
# Store this record in the cache.
|
151
|
+
# Store a new copy of ourselves into the cache.
|
161
152
|
|
162
|
-
def
|
163
|
-
|
164
|
-
|
153
|
+
def update
|
154
|
+
return super
|
155
|
+
ensure
|
156
|
+
cache_store
|
165
157
|
end
|
166
158
|
|
167
159
|
##
|
@@ -176,7 +168,7 @@ class CachedModel < ActiveRecord::Base
|
|
176
168
|
# The local cache key for this record.
|
177
169
|
|
178
170
|
def cache_key_local
|
179
|
-
return "#{self.class
|
171
|
+
return "#{self.class}:#{id}"
|
180
172
|
end
|
181
173
|
|
182
174
|
##
|
@@ -186,5 +178,23 @@ class CachedModel < ActiveRecord::Base
|
|
186
178
|
return "#{KEY}:#{cache_key_local}"
|
187
179
|
end
|
188
180
|
|
181
|
+
##
|
182
|
+
# The local object cache.
|
183
|
+
|
184
|
+
def cache_local
|
185
|
+
return CachedModel.cache_local
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# Store this record in the cache without associations. Storing associations
|
190
|
+
# leads to wasted cache space and hard-to-debug problems.
|
191
|
+
|
192
|
+
def cache_store
|
193
|
+
obj = dup
|
194
|
+
obj.send :instance_variable_set, :@attributes, attributes_before_type_cast
|
195
|
+
cache_local[cache_key_local] = obj
|
196
|
+
Cache.put cache_key_memcache, obj, TTL
|
197
|
+
end
|
198
|
+
|
189
199
|
end
|
190
200
|
|
@@ -0,0 +1,305 @@
|
|
1
|
+
require 'test/unit'
|
2
|
+
|
3
|
+
$TESTING = :cached_model
|
4
|
+
|
5
|
+
RAILS_ENV = 'production'
|
6
|
+
|
7
|
+
module ActiveRecord; end
|
8
|
+
|
9
|
+
class ActiveRecord::Base
|
10
|
+
|
11
|
+
@count = 1000
|
12
|
+
|
13
|
+
attr_accessor :id, :attributes
|
14
|
+
|
15
|
+
def self.next_id
|
16
|
+
@count += 1
|
17
|
+
return @count
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.table_name
|
21
|
+
name.downcase
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.primary_key
|
25
|
+
'id'
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.find(*args)
|
29
|
+
args.flatten!
|
30
|
+
return [new] if args.length == 1 and Fixnum === args.first
|
31
|
+
return new if args.length == 2
|
32
|
+
return [new, new, new] if args.length == 3
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.find_by_sql(query)
|
36
|
+
return [new]
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize
|
40
|
+
@id = ActiveRecord::Base.next_id
|
41
|
+
@attributes = { :data => 'data' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(other)
|
45
|
+
self.class == other.class and
|
46
|
+
id == other.id and
|
47
|
+
attributes_before_type_cast == other.attributes_before_type_cast
|
48
|
+
end
|
49
|
+
|
50
|
+
def attributes_before_type_cast
|
51
|
+
{ :data => @attributes[:data] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def destroy
|
55
|
+
end
|
56
|
+
|
57
|
+
def reload
|
58
|
+
@attributes[:data].succ!
|
59
|
+
@attributes[:extra] = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def update
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
require 'cached_model'
|
68
|
+
|
69
|
+
class Concrete < CachedModel; end
|
70
|
+
class STILameness < Concrete; end
|
71
|
+
|
72
|
+
module Cache
|
73
|
+
|
74
|
+
@cache = {}
|
75
|
+
@ttl = {}
|
76
|
+
|
77
|
+
class << self; attr_accessor :cache, :ttl; end
|
78
|
+
|
79
|
+
def self.delete(key)
|
80
|
+
@cache.delete key
|
81
|
+
@ttl.delete key
|
82
|
+
return nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.get(key)
|
86
|
+
@cache[key]
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.put(key, value, ttl)
|
90
|
+
value = Marshal.load Marshal.dump(value)
|
91
|
+
@cache[key] = value
|
92
|
+
@ttl[key] = ttl
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
class << CachedModel
|
98
|
+
|
99
|
+
attr_writer :cache_local
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
class TestCachedModel < Test::Unit::TestCase
|
104
|
+
|
105
|
+
def setup
|
106
|
+
@model = Concrete.new
|
107
|
+
Cache.cache = {}
|
108
|
+
Cache.ttl = {}
|
109
|
+
CachedModel.cache_local = {}
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_class_cache_delete
|
113
|
+
util_set
|
114
|
+
|
115
|
+
CachedModel.cache_delete @model.class, @model.id
|
116
|
+
|
117
|
+
assert_equal true, CachedModel.cache_local.empty?
|
118
|
+
assert_equal true, Cache.cache.empty?
|
119
|
+
end
|
120
|
+
|
121
|
+
def test_class_cache_reset
|
122
|
+
util_set
|
123
|
+
|
124
|
+
CachedModel.cache_reset
|
125
|
+
|
126
|
+
assert_equal true, CachedModel.cache_local.empty?
|
127
|
+
assert_equal false, Cache.cache.empty?
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_class_descends_from_active_record?
|
131
|
+
assert_equal true, Concrete.descends_from_active_record?
|
132
|
+
assert_equal false, STILameness.descends_from_active_record?
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_class_find_complex
|
136
|
+
record = Concrete.find(1, :order => 'lock_version')
|
137
|
+
assert_equal @model.id + 1, record.id
|
138
|
+
|
139
|
+
assert_equal record, CachedModel.cache_local[record.cache_key_local]
|
140
|
+
assert_equal record, Cache.cache[record.cache_key_memcache]
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_class_find_in_local_cache
|
144
|
+
util_set
|
145
|
+
|
146
|
+
record = Concrete.find(1, :order => 'lock_version')
|
147
|
+
assert_equal @model.id + 1, record.id
|
148
|
+
|
149
|
+
assert_equal record, CachedModel.cache_local[record.cache_key_local]
|
150
|
+
assert_equal record, Cache.cache[record.cache_key_memcache]
|
151
|
+
end
|
152
|
+
|
153
|
+
def test_class_find_in_memcache
|
154
|
+
util_set
|
155
|
+
CachedModel.cache_reset
|
156
|
+
|
157
|
+
record = Concrete.find @model.id
|
158
|
+
assert_equal @model, record
|
159
|
+
|
160
|
+
assert_equal record, CachedModel.cache_local[record.cache_key_local]
|
161
|
+
end
|
162
|
+
|
163
|
+
def test_class_find_multiple
|
164
|
+
ids = [@model.id + 1, @model.id + 2, @model.id + 3]
|
165
|
+
records = Concrete.find(*ids)
|
166
|
+
assert_equal ids, records.map { |r| r.id }
|
167
|
+
|
168
|
+
assert_equal ids.map { |i| "#{@model.class}:#{i}" },
|
169
|
+
CachedModel.cache_local.keys
|
170
|
+
assert_equal ids.map { |i| "#{CachedModel::KEY}:#{@model.class}:#{i}" },
|
171
|
+
Cache.cache.keys
|
172
|
+
end
|
173
|
+
|
174
|
+
def test_class_find_not_cached
|
175
|
+
record = Concrete.find @model.id + 1
|
176
|
+
assert_equal @model.id + 1, record.id
|
177
|
+
|
178
|
+
assert_equal record, CachedModel.cache_local[record.cache_key_local]
|
179
|
+
assert_equal record, Cache.cache[record.cache_key_memcache]
|
180
|
+
end
|
181
|
+
|
182
|
+
def test_class_find_by_sql
|
183
|
+
q = "SELECT * FROM concrete WHERE (concrete.id = #{@model.id + 1}) LIMIT 1"
|
184
|
+
record = Concrete.find_by_sql(q).first
|
185
|
+
assert_equal @model.id + 1, record.id
|
186
|
+
|
187
|
+
assert_equal record, CachedModel.cache_local[record.cache_key_local]
|
188
|
+
assert_equal record, Cache.cache[record.cache_key_memcache]
|
189
|
+
end
|
190
|
+
|
191
|
+
def test_class_find_by_sql_skip_hack
|
192
|
+
Concrete.instance_variable_set :@skip_find_hack, true
|
193
|
+
q = "SELECT * FROM concrete WHERE (concrete.id = #{@model.id + 1}) LIMIT 1"
|
194
|
+
record = Concrete.find_by_sql(q).first
|
195
|
+
assert_equal @model.id + 1, record.id
|
196
|
+
|
197
|
+
assert_equal true, CachedModel.cache_local.empty?
|
198
|
+
assert_equal true, Cache.cache.empty?
|
199
|
+
ensure
|
200
|
+
Concrete.instance_variable_set :@skip_find_hack, false
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_class_name_of_active_record_descendant
|
204
|
+
assert_equal "Concrete",
|
205
|
+
CachedModel.class_name_of_active_record_descendant(Concrete)
|
206
|
+
assert_equal "Concrete",
|
207
|
+
CachedModel.class_name_of_active_record_descendant(STILameness)
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_cache_delete
|
211
|
+
util_set
|
212
|
+
|
213
|
+
@model.cache_delete
|
214
|
+
|
215
|
+
assert_equal true, CachedModel.cache_local.empty?
|
216
|
+
assert_equal true, Cache.cache.empty?
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_cache_key_local
|
220
|
+
assert_equal "#{@model.class}:#{@model.id}", @model.cache_key_local
|
221
|
+
end
|
222
|
+
|
223
|
+
def test_cache_key_memcache
|
224
|
+
assert_equal "#{CachedModel::KEY}:#{@model.class}:#{@model.id}",
|
225
|
+
@model.cache_key_memcache
|
226
|
+
end
|
227
|
+
|
228
|
+
def test_cache_local
|
229
|
+
assert_same CachedModel.cache_local, @model.cache_local
|
230
|
+
end
|
231
|
+
|
232
|
+
def test_cache_store
|
233
|
+
@model.cache_store
|
234
|
+
assert_equal false, CachedModel.cache_local.empty?
|
235
|
+
assert_equal false, Cache.cache.empty?
|
236
|
+
|
237
|
+
assert_equal @model, CachedModel.cache_local[@model.cache_key_local]
|
238
|
+
assert_equal @model, Cache.cache[@model.cache_key_memcache]
|
239
|
+
assert_equal CachedModel::TTL, Cache.ttl[@model.cache_key_memcache]
|
240
|
+
end
|
241
|
+
|
242
|
+
def test_cache_store_with_attributes
|
243
|
+
@model.attributes[:extra] = 'extra'
|
244
|
+
@model.cache_store
|
245
|
+
assert_equal false, CachedModel.cache_local.empty?
|
246
|
+
assert_equal false, Cache.cache.empty?
|
247
|
+
|
248
|
+
local_model = CachedModel.cache_local[@model.cache_key_local]
|
249
|
+
mem_model = Cache.cache[@model.cache_key_memcache]
|
250
|
+
assert_equal @model, local_model
|
251
|
+
assert_equal @model, mem_model
|
252
|
+
assert_equal CachedModel::TTL, Cache.ttl[@model.cache_key_memcache]
|
253
|
+
|
254
|
+
expected = {:data => 'data'}
|
255
|
+
|
256
|
+
assert_equal expected, local_model.attributes
|
257
|
+
assert_equal expected, mem_model.attributes
|
258
|
+
end
|
259
|
+
|
260
|
+
def test_destroy
|
261
|
+
util_set
|
262
|
+
|
263
|
+
@model.destroy
|
264
|
+
|
265
|
+
assert_equal true, CachedModel.cache_local.empty?
|
266
|
+
assert_equal true, Cache.cache.empty?
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_reload
|
270
|
+
util_set
|
271
|
+
|
272
|
+
@model.reload
|
273
|
+
|
274
|
+
assert_equal false, CachedModel.cache_local.empty?
|
275
|
+
assert_equal false, Cache.cache.empty?
|
276
|
+
|
277
|
+
assert_equal 'datb',
|
278
|
+
CachedModel.cache_local[@model.cache_key_local].attributes[:data]
|
279
|
+
assert_equal 'datb',
|
280
|
+
Cache.cache[@model.cache_key_memcache].attributes[:data]
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_update
|
284
|
+
util_set
|
285
|
+
|
286
|
+
@model.attributes[:data] = 'atad'
|
287
|
+
@model.update
|
288
|
+
|
289
|
+
assert_equal false, CachedModel.cache_local.empty?
|
290
|
+
assert_equal false, Cache.cache.empty?
|
291
|
+
|
292
|
+
assert_equal 'atad',
|
293
|
+
CachedModel.cache_local[@model.cache_key_local].attributes[:data]
|
294
|
+
assert_equal 'atad',
|
295
|
+
Cache.cache[@model.cache_key_memcache].attributes[:data]
|
296
|
+
end
|
297
|
+
|
298
|
+
def util_set(klass = @model.class, id = @model.id, data = @model)
|
299
|
+
key = "#{klass}:#{id}"
|
300
|
+
CachedModel.cache_local[key] = data
|
301
|
+
Cache.cache["#{CachedModel::KEY}:#{key}"] = data
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.11.6
|
|
3
3
|
specification_version: 1
|
4
4
|
name: cached_model
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 1.0.
|
7
|
-
date: 2006-01-
|
6
|
+
version: 1.0.1
|
7
|
+
date: 2006-01-30 00:00:00 -08:00
|
8
8
|
summary: An ActiveRecord::Base model that caches records
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -28,10 +28,12 @@ cert_chain:
|
|
28
28
|
authors:
|
29
29
|
- Robert Cottrell, Eric Hodel
|
30
30
|
files:
|
31
|
+
- LICENSE
|
31
32
|
- Manifest.txt
|
32
33
|
- README
|
33
34
|
- Rakefile
|
34
35
|
- lib/cached_model.rb
|
36
|
+
- test/test_cached_model.rb
|
35
37
|
test_files: []
|
36
38
|
|
37
39
|
rdoc_options: []
|