cached_model 1.0.1 → 1.1.0

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 CHANGED
@@ -4,12 +4,30 @@ Rubyforge Project:
4
4
 
5
5
  http://rubyforge.org/projects/rctools/
6
6
 
7
+ Documentation:
8
+
9
+ http://dev.robotcoop.com/Libraries/cached_model/
10
+
7
11
  == About
8
12
 
9
13
  CachedModel stores Rails ActiveRecord objects in memcache allowing for very
10
14
  fast retrievals. CachedModel uses the ActiveRecord::Locking to ensure that
11
15
  you don't perform multiple updates.
12
16
 
17
+ == CachedModel Doesn't...
18
+
19
+ CachedModel is not magic.
20
+
21
+ CachedModel only accelerates simple finds for single rows.
22
+
23
+ CachedModel won't cache every query you run.
24
+
25
+ CachedModel isn't smart enough to determine the dependencies between your
26
+ queries so that it can accelerate more complicated queries. Without these
27
+ smarts you'll only end up with a broken application.
28
+
29
+ If you want to cache more complicated queries you need do it by hand.
30
+
13
31
  == Using CachedModel
14
32
 
15
33
  First, install the cached_model gem:
@@ -50,7 +68,7 @@ Then make Rails load the gem:
50
68
  $ tail -n 4 config/environment.rb
51
69
  # Include your application configuration below
52
70
 
53
- require_gem 'cached_model'
71
+ require 'cached_model'
54
72
 
55
73
  Then edit your ActiveRecord model to inherit from CachedModel instead of
56
74
  ActiveRecord::Base:
@@ -64,3 +82,33 @@ ActiveRecord::Base:
64
82
  belongs_to :point
65
83
  belongs_to :route
66
84
 
85
+ == Extra Features
86
+
87
+ === Local Cache
88
+
89
+ CachedModel also incorporates an in-process cache, but it is disabled by
90
+ default. In order to enable the in-process cache enable it in your
91
+ environment.rb:
92
+
93
+ CachedModel.use_local_cache = true
94
+
95
+ And add a after_filter that flushes the local cache:
96
+
97
+ class ApplicationController < ActionController::Base
98
+
99
+ after_filter { CachedModel.cache_reset }
100
+
101
+ end
102
+
103
+ IF YOU DO NOT ADD THE AFTER FILTER YOU WILL EXPERIENCE EXTREME PROCESS GROWTH.
104
+
105
+ === Memcache
106
+
107
+ Memcache may be disabled and the TTL for objects stored in memcache may be
108
+ changed:
109
+
110
+ CachedModel.use_memcache = false
111
+ CachedModel.ttl = 86400
112
+
113
+ The default TTL for memcache objects is 900 seconds.
114
+
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.1'
11
+ s.version = '1.1.0'
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'
@@ -9,12 +9,53 @@ require 'memcache_util' unless $TESTING == :cached_model
9
9
 
10
10
  class CachedModel < ActiveRecord::Base
11
11
 
12
- KEY = 'active_record'
13
- TTL = 60 * 15 # seconds
14
-
15
12
  @cache_local = {}
13
+ @use_local_cache = false
14
+ @use_memcache = true
15
+ @ttl = 60 * 15
16
16
 
17
- class << self; attr_reader :cache_local; end
17
+ class << self
18
+
19
+ ##
20
+ # The local process cache. You shouldn't touch me.
21
+
22
+ attr_reader :cache_local
23
+
24
+ ##
25
+ # Enables or disables use of the local cache.
26
+ #
27
+ # NOTE if you enable this you must call #cache_reset or you will
28
+ # experience uncontrollable process growth!
29
+ #
30
+ # Defaults to false.
31
+
32
+ attr_writer :use_local_cache
33
+
34
+ ##
35
+ # Enables or disables the use of memcache.
36
+
37
+ attr_writer :use_memcache
38
+
39
+ ##
40
+ # Memcache record time-to-live for stored records.
41
+
42
+ attr_accessor :ttl
43
+
44
+ ##
45
+ # Returns true if use of the local cache is enabled.
46
+
47
+ def use_local_cache?
48
+ return @use_local_cache
49
+ end
50
+
51
+ ##
52
+ # Returns true if use of memcache is enabled.
53
+
54
+ def use_memcache?
55
+ return @use_memcache
56
+ end
57
+
58
+ end
18
59
 
19
60
  ##
20
61
  # Override the flawed assumption ActiveRecord::Base makes about inheritance.
@@ -22,7 +63,7 @@ class CachedModel < ActiveRecord::Base
22
63
  def self.descends_from_active_record?
23
64
  superclass == CachedModel
24
65
  end
25
-
66
+
26
67
  ##
27
68
  # Override the flawed assumption ActiveRecord::Base makes about inheritance.
28
69
 
@@ -36,21 +77,21 @@ class CachedModel < ActiveRecord::Base
36
77
  end
37
78
  end
38
79
 
39
- ##
80
+ ##
40
81
  # Invalidate the cache entry for an record. The update method will
41
82
  # automatically invalidate the cache when updates are made through
42
83
  # ActiveRecord model record. However, several methods update tables with
43
84
  # direct sql queries for effeciency. These methods should call this method
44
85
  # to invalidate the cache after making those changes.
45
- #
86
+ #
46
87
  # NOTE - if a SQL query updates multiple rows with one query, there is
47
88
  # currently no way to invalidate the affected entries unless the entire
48
89
  # cache is dumped or until the TTL expires, so try not to do this.
49
90
 
50
91
  def self.cache_delete(klass, id)
51
92
  key = "#{klass}:#{id}"
52
- CachedModel.cache_local.delete key
53
- Cache.delete "#{KEY}:#{key}"
93
+ CachedModel.cache_local.delete key if CachedModel.use_local_cache?
94
+ Cache.delete "active_record:#{key}" if CachedModel.use_memcache?
54
95
  end
55
96
 
56
97
  ##
@@ -58,7 +99,7 @@ class CachedModel < ActiveRecord::Base
58
99
  # filter at the beginning of each request.
59
100
 
60
101
  def self.cache_reset
61
- CachedModel.cache_local.clear
102
+ CachedModel.cache_local.clear if CachedModel.use_local_cache?
62
103
  end
63
104
 
64
105
  ##
@@ -73,12 +114,14 @@ class CachedModel < ActiveRecord::Base
73
114
  if ($TESTING and $TESTING != :cached_model) or
74
115
  args.length != 1 or not Fixnum === args.first then
75
116
  records = super
117
+ # Rails requires two levels of indirection to look up a record
118
+ return records if args.first == :all and @skip_find_hack
76
119
  return records if RAILS_ENV == 'test'
77
120
  case records
78
121
  when Array then
79
122
  records.each { |r| r.cache_store }
80
123
  when CachedModel then
81
- records.cache_store
124
+ records.cache_store # Model.find 1 gets cached here
82
125
  end
83
126
  return records
84
127
  end
@@ -86,26 +129,33 @@ class CachedModel < ActiveRecord::Base
86
129
  # Try to find the record in the local cache.
87
130
  id = args.first
88
131
  cache_key_local = "#{name}:#{id}"
89
- record = CachedModel.cache_local[cache_key_local]
90
- return record unless record.nil?
132
+ if CachedModel.use_local_cache? then
133
+ record = CachedModel.cache_local[cache_key_local]
134
+ return record unless record.nil?
135
+ end
91
136
 
92
137
  # Try to find the record in memcache and add it to the local cache
93
- record = Cache.get "#{KEY}:#{cache_key_local}"
94
- unless record.nil? then
95
- CachedModel.cache_local[cache_key_local] = record
96
- return record
138
+ if CachedModel.use_memcache? then
139
+ record = Cache.get "active_record:#{cache_key_local}"
140
+ unless record.nil? then
141
+ if CachedModel.use_local_cache? then
142
+ CachedModel.cache_local[cache_key_local] = record
143
+ end
144
+ return record
145
+ end
97
146
  end
98
147
 
99
- # Fetch the record from the DB and cache it.
148
+ # Fetch the record from the DB. Inside the mulitple levels of indirection
149
+ # of find it will get cached.
100
150
  #
101
151
  # We don't want the subsequent find_by_sql to loop back here, so guard
102
152
  # the call.
103
153
  #
104
- # NOTE - this guard isn't thread safe.
154
+ # NOTE This guard is not thread safe, beware use of CachedModel where
155
+ # ActiveRecord's thread safety is disabled.
105
156
  begin
106
157
  @skip_find_hack = true
107
158
  record = super(args).first
108
- record.cache_store
109
159
  ensure
110
160
  @skip_find_hack = false
111
161
  end
@@ -160,8 +210,8 @@ class CachedModel < ActiveRecord::Base
160
210
  # Remove this record from the cache.
161
211
 
162
212
  def cache_delete
163
- cache_local.delete cache_key_local
164
- Cache.delete cache_key_memcache
213
+ cache_local.delete cache_key_local if CachedModel.use_local_cache?
214
+ Cache.delete cache_key_memcache if CachedModel.use_memcache?
165
215
  end
166
216
 
167
217
  ##
@@ -175,7 +225,7 @@ class CachedModel < ActiveRecord::Base
175
225
  # The memcache key for this record.
176
226
 
177
227
  def cache_key_memcache
178
- return "#{KEY}:#{cache_key_local}"
228
+ return "active_record:#{cache_key_local}"
179
229
  end
180
230
 
181
231
  ##
@@ -192,8 +242,12 @@ class CachedModel < ActiveRecord::Base
192
242
  def cache_store
193
243
  obj = dup
194
244
  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
245
+ if CachedModel.use_local_cache? then
246
+ cache_local[cache_key_local] = obj
247
+ end
248
+ if CachedModel.use_memcache? then
249
+ Cache.put cache_key_memcache, obj, CachedModel.ttl
250
+ end
197
251
  end
198
252
 
199
253
  end
@@ -12,6 +12,32 @@ class ActiveRecord::Base
12
12
 
13
13
  attr_accessor :id, :attributes
14
14
 
15
+ def self.find(*args)
16
+ args.flatten!
17
+ case args.first
18
+ when :first then
19
+ return find(:all, *args)
20
+ when :all then
21
+ [new]
22
+ else
23
+ case args.length
24
+ when 1 then
25
+ return find(:first, *args) if Fixnum === args.first
26
+ raise "Dunno what to do"
27
+ when 2 then
28
+ return new
29
+ when 3 then
30
+ return [new, new, new]
31
+ else
32
+ raise "Dunno what to do"
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.find_by_sql(query)
38
+ return [new]
39
+ end
40
+
15
41
  def self.next_id
16
42
  @count += 1
17
43
  return @count
@@ -25,17 +51,6 @@ class ActiveRecord::Base
25
51
  'id'
26
52
  end
27
53
 
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
54
  def initialize
40
55
  @id = ActiveRecord::Base.next_id
41
56
  @attributes = { :data => 'data' }
@@ -102,14 +117,26 @@ end
102
117
 
103
118
  class TestCachedModel < Test::Unit::TestCase
104
119
 
120
+ DEFAULT_USE_LOCAL_CACHE = CachedModel.use_local_cache?
121
+ DEFAULT_USE_MEMCACHE = CachedModel.use_memcache?
122
+
105
123
  def setup
106
124
  @model = Concrete.new
107
125
  Cache.cache = {}
108
126
  Cache.ttl = {}
109
127
  CachedModel.cache_local = {}
128
+ CachedModel.use_local_cache = DEFAULT_USE_LOCAL_CACHE
129
+ CachedModel.use_memcache = DEFAULT_USE_MEMCACHE
130
+ end
131
+
132
+ def teardown
133
+ CachedModel.use_local_cache = DEFAULT_USE_LOCAL_CACHE
134
+ CachedModel.use_memcache = DEFAULT_USE_MEMCACHE
110
135
  end
111
136
 
112
137
  def test_class_cache_delete
138
+ CachedModel.use_local_cache = true
139
+ CachedModel.use_memcache = true
113
140
  util_set
114
141
 
115
142
  CachedModel.cache_delete @model.class, @model.id
@@ -118,7 +145,53 @@ class TestCachedModel < Test::Unit::TestCase
118
145
  assert_equal true, Cache.cache.empty?
119
146
  end
120
147
 
148
+ def test_class_cache_delete_without_local_cache
149
+ CachedModel.use_local_cache = false
150
+ CachedModel.use_memcache = true
151
+ util_set
152
+
153
+ CachedModel.cache_delete @model.class, @model.id
154
+
155
+ assert_equal false, CachedModel.cache_local.empty?
156
+ assert_equal true, Cache.cache.empty?
157
+ end
158
+
159
+ def test_class_cache_delete_without_memcache
160
+ CachedModel.use_local_cache = true
161
+ CachedModel.use_memcache = false
162
+ util_set
163
+
164
+ CachedModel.cache_delete @model.class, @model.id
165
+
166
+ assert_equal true, CachedModel.cache_local.empty?
167
+ assert_equal false, Cache.cache.empty?
168
+ end
169
+
121
170
  def test_class_cache_reset
171
+ CachedModel.use_local_cache = true
172
+ CachedModel.use_memcache = true
173
+ util_set
174
+
175
+ CachedModel.cache_reset
176
+
177
+ assert_equal true, CachedModel.cache_local.empty?
178
+ assert_equal false, Cache.cache.empty?
179
+ end
180
+
181
+ def test_class_cache_reset_without_local_cache
182
+ CachedModel.use_local_cache = false
183
+ CachedModel.use_memcache = true
184
+ util_set
185
+
186
+ CachedModel.cache_reset
187
+
188
+ assert_equal false, CachedModel.cache_local.empty?
189
+ assert_equal false, Cache.cache.empty?
190
+ end
191
+
192
+ def test_class_cache_reset_without_memcache
193
+ CachedModel.use_local_cache = true
194
+ CachedModel.use_memcache = false
122
195
  util_set
123
196
 
124
197
  CachedModel.cache_reset
@@ -133,45 +206,78 @@ class TestCachedModel < Test::Unit::TestCase
133
206
  end
134
207
 
135
208
  def test_class_find_complex
209
+ CachedModel.use_local_cache = true
210
+ CachedModel.use_memcache = true
211
+
136
212
  record = Concrete.find(1, :order => 'lock_version')
213
+
137
214
  assert_equal @model.id + 1, record.id
138
215
 
139
216
  assert_equal record, CachedModel.cache_local[record.cache_key_local]
140
217
  assert_equal record, Cache.cache[record.cache_key_memcache]
141
218
  end
142
219
 
220
+ def test_class_find_complex_without_local_cache
221
+ CachedModel.use_local_cache = false
222
+ CachedModel.use_memcache = true
223
+
224
+ record = Concrete.find(1, :order => 'lock_version')
225
+
226
+ assert_equal @model.id + 1, record.id
227
+
228
+ assert_equal nil, CachedModel.cache_local[record.cache_key_local]
229
+ assert_equal record, Cache.cache[record.cache_key_memcache]
230
+ end
231
+
232
+ def test_class_find_complex_without_memcache
233
+ CachedModel.use_local_cache = true
234
+ CachedModel.use_memcache = false
235
+
236
+ record = Concrete.find(1, :order => 'lock_version')
237
+
238
+ assert_equal @model.id + 1, record.id
239
+
240
+ assert_equal record, CachedModel.cache_local[record.cache_key_local]
241
+ assert_equal nil, Cache.cache[record.cache_key_memcache]
242
+ end
243
+
143
244
  def test_class_find_in_local_cache
245
+ CachedModel.use_local_cache = true
144
246
  util_set
145
247
 
146
248
  record = Concrete.find(1, :order => 'lock_version')
147
249
  assert_equal @model.id + 1, record.id
148
250
 
149
251
  assert_equal record, CachedModel.cache_local[record.cache_key_local]
150
- assert_equal record, Cache.cache[record.cache_key_memcache]
151
252
  end
152
253
 
153
254
  def test_class_find_in_memcache
255
+ CachedModel.use_local_cache = true
256
+ CachedModel.use_memcache = true
154
257
  util_set
155
258
  CachedModel.cache_reset
156
259
 
157
260
  record = Concrete.find @model.id
261
+
158
262
  assert_equal @model, record
159
263
 
160
264
  assert_equal record, CachedModel.cache_local[record.cache_key_local]
161
265
  end
162
266
 
163
267
  def test_class_find_multiple
268
+ CachedModel.use_local_cache = true
164
269
  ids = [@model.id + 1, @model.id + 2, @model.id + 3]
165
270
  records = Concrete.find(*ids)
166
271
  assert_equal ids, records.map { |r| r.id }
167
272
 
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
273
+ assert_equal ids.map { |i| "#{@model.class}:#{i}" }.sort,
274
+ CachedModel.cache_local.keys.sort
275
+ assert_equal ids.map { |i| "active_record:#{@model.class}:#{i}" }.sort,
276
+ Cache.cache.keys.sort
172
277
  end
173
278
 
174
279
  def test_class_find_not_cached
280
+ CachedModel.use_local_cache = true
175
281
  record = Concrete.find @model.id + 1
176
282
  assert_equal @model.id + 1, record.id
177
283
 
@@ -180,6 +286,7 @@ class TestCachedModel < Test::Unit::TestCase
180
286
  end
181
287
 
182
288
  def test_class_find_by_sql
289
+ CachedModel.use_local_cache = true
183
290
  q = "SELECT * FROM concrete WHERE (concrete.id = #{@model.id + 1}) LIMIT 1"
184
291
  record = Concrete.find_by_sql(q).first
185
292
  assert_equal @model.id + 1, record.id
@@ -207,7 +314,28 @@ class TestCachedModel < Test::Unit::TestCase
207
314
  CachedModel.class_name_of_active_record_descendant(STILameness)
208
315
  end
209
316
 
317
+ def test_class_ttl
318
+ assert_equal 900, CachedModel.ttl
319
+ CachedModel.ttl = 800
320
+ assert_equal 800, CachedModel.ttl
321
+ end
322
+
323
+ def test_class_use_local_cache_eh
324
+ CachedModel.use_local_cache = true
325
+ assert_equal true, CachedModel.use_local_cache?
326
+ CachedModel.use_local_cache = false
327
+ assert_equal false, CachedModel.use_local_cache?
328
+ end
329
+
330
+ def test_class_use_memcache_eh
331
+ CachedModel.use_memcache = true
332
+ assert_equal true, CachedModel.use_memcache?
333
+ CachedModel.use_memcache = false
334
+ assert_equal false, CachedModel.use_memcache?
335
+ end
336
+
210
337
  def test_cache_delete
338
+ CachedModel.use_local_cache = true
211
339
  util_set
212
340
 
213
341
  @model.cache_delete
@@ -221,7 +349,7 @@ class TestCachedModel < Test::Unit::TestCase
221
349
  end
222
350
 
223
351
  def test_cache_key_memcache
224
- assert_equal "#{CachedModel::KEY}:#{@model.class}:#{@model.id}",
352
+ assert_equal "active_record:#{@model.class}:#{@model.id}",
225
353
  @model.cache_key_memcache
226
354
  end
227
355
 
@@ -230,16 +358,48 @@ class TestCachedModel < Test::Unit::TestCase
230
358
  end
231
359
 
232
360
  def test_cache_store
361
+ CachedModel.use_local_cache = true
362
+ CachedModel.use_memcache = true
363
+
233
364
  @model.cache_store
365
+
234
366
  assert_equal false, CachedModel.cache_local.empty?
235
367
  assert_equal false, Cache.cache.empty?
236
368
 
237
369
  assert_equal @model, CachedModel.cache_local[@model.cache_key_local]
238
370
  assert_equal @model, Cache.cache[@model.cache_key_memcache]
239
- assert_equal CachedModel::TTL, Cache.ttl[@model.cache_key_memcache]
371
+ assert_equal CachedModel.ttl, Cache.ttl[@model.cache_key_memcache]
372
+ end
373
+
374
+ def test_cache_store_without_local_cache
375
+ CachedModel.use_local_cache = false
376
+ CachedModel.use_memcache = true
377
+
378
+ @model.cache_store
379
+
380
+ assert_equal true, CachedModel.cache_local.empty?
381
+ assert_equal false, Cache.cache.empty?
382
+
383
+ assert_equal nil, CachedModel.cache_local[@model.cache_key_local]
384
+ assert_equal @model, Cache.cache[@model.cache_key_memcache]
385
+ assert_equal CachedModel.ttl, Cache.ttl[@model.cache_key_memcache]
386
+ end
387
+
388
+ def test_cache_store_without_memcache
389
+ CachedModel.use_local_cache = true
390
+ CachedModel.use_memcache = false
391
+
392
+ @model.cache_store
393
+
394
+ assert_equal false, CachedModel.cache_local.empty?
395
+ assert_equal true, Cache.cache.empty?
396
+
397
+ assert_equal @model, CachedModel.cache_local[@model.cache_key_local]
398
+ assert_equal nil, Cache.cache[@model.cache_key_memcache]
240
399
  end
241
400
 
242
401
  def test_cache_store_with_attributes
402
+ CachedModel.use_local_cache = true
243
403
  @model.attributes[:extra] = 'extra'
244
404
  @model.cache_store
245
405
  assert_equal false, CachedModel.cache_local.empty?
@@ -249,7 +409,7 @@ class TestCachedModel < Test::Unit::TestCase
249
409
  mem_model = Cache.cache[@model.cache_key_memcache]
250
410
  assert_equal @model, local_model
251
411
  assert_equal @model, mem_model
252
- assert_equal CachedModel::TTL, Cache.ttl[@model.cache_key_memcache]
412
+ assert_equal CachedModel.ttl, Cache.ttl[@model.cache_key_memcache]
253
413
 
254
414
  expected = {:data => 'data'}
255
415
 
@@ -258,6 +418,7 @@ class TestCachedModel < Test::Unit::TestCase
258
418
  end
259
419
 
260
420
  def test_destroy
421
+ CachedModel.use_local_cache = true
261
422
  util_set
262
423
 
263
424
  @model.destroy
@@ -298,7 +459,7 @@ class TestCachedModel < Test::Unit::TestCase
298
459
  def util_set(klass = @model.class, id = @model.id, data = @model)
299
460
  key = "#{klass}:#{id}"
300
461
  CachedModel.cache_local[key] = data
301
- Cache.cache["#{CachedModel::KEY}:#{key}"] = data
462
+ Cache.cache["active_record:#{key}"] = data
302
463
  end
303
464
 
304
465
  end
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.1
7
- date: 2006-01-30 00:00:00 -08:00
6
+ version: 1.1.0
7
+ date: 2006-02-19 00:00:00 -08:00
8
8
  summary: An ActiveRecord::Base model that caches records
9
9
  require_paths:
10
10
  - lib
@@ -25,6 +25,7 @@ required_ruby_version: !ruby/object:Gem::Version::Requirement
25
25
  platform: ruby
26
26
  signing_key:
27
27
  cert_chain:
28
+ post_install_message:
28
29
  authors:
29
30
  - Robert Cottrell, Eric Hodel
30
31
  files: