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 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
+
@@ -1,4 +1,6 @@
1
+ LICENSE
1
2
  Manifest.txt
2
3
  README
3
4
  Rakefile
4
5
  lib/cached_model.rb
6
+ test/test_cached_model.rb
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.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
@@ -1,4 +1,7 @@
1
- require 'memcache_util'
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 per-request cache. This should be called from a before
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 or args.length != 1 or not Fixnum === args.first then
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 != 'test' and Array === records then
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 [self.find($1.to_i)]
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
- # The local object cache.
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 cache_store
163
- cache_local[cache_key_memcache] = self
164
- Cache.put cache_key_memcache, self, TTL
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.name}:#{self.id}"
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.0
7
- date: 2006-01-17 00:00:00 -08:00
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: []