cached_model 1.0.1 → 1.1.0

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