cached_model 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|