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 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: []