cached_model 1.0.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/Manifest.txt +4 -0
- data/README +66 -0
- data/Rakefile +57 -0
- data/lib/cached_model.rb +190 -0
- metadata +56 -0
data/Manifest.txt
ADDED
data/README
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
= CachedModel
|
2
|
+
|
3
|
+
Rubyforge Project:
|
4
|
+
|
5
|
+
http://rubyforge.org/projects/rctools/
|
6
|
+
|
7
|
+
== About
|
8
|
+
|
9
|
+
CachedModel stores Rails ActiveRecord objects in memcache allowing for very
|
10
|
+
fast retrievals. CachedModel uses the ActiveRecord::Locking to ensure that
|
11
|
+
you don't perform multiple updates.
|
12
|
+
|
13
|
+
== Using CachedModel
|
14
|
+
|
15
|
+
First, install the cached_model gem:
|
16
|
+
|
17
|
+
$ sudo gem install cached_model
|
18
|
+
|
19
|
+
Then set up memcache-client:
|
20
|
+
|
21
|
+
$ tail -n 20 config/environments/production.rb
|
22
|
+
memcache_options = {
|
23
|
+
:c_threshold => 10_000,
|
24
|
+
:compression => true,
|
25
|
+
:debug => false,
|
26
|
+
:namespace => 'my_rails_app',
|
27
|
+
:readonly => false,
|
28
|
+
:urlencode => false
|
29
|
+
}
|
30
|
+
|
31
|
+
CACHE = MemCache.new memcache_options
|
32
|
+
CACHE.servers = 'localhost:11211'
|
33
|
+
|
34
|
+
session_options = {
|
35
|
+
:database_manager => CGI::Session::MemCacheStore,
|
36
|
+
:cache => CACHE,
|
37
|
+
:session_domain => 'trackmap.robotcoop.com'
|
38
|
+
}
|
39
|
+
|
40
|
+
ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS.update session_options
|
41
|
+
|
42
|
+
You will need separate namespaces for production and development, if using
|
43
|
+
memcache on the same machine.
|
44
|
+
|
45
|
+
Note that using memcache with tests will cause test failures, so set your
|
46
|
+
memcache to be readonly for the test environment.
|
47
|
+
|
48
|
+
Then make Rails load the gem:
|
49
|
+
|
50
|
+
$ tail -n 4 config/environment.rb
|
51
|
+
# Include your application configuration below
|
52
|
+
|
53
|
+
require_gem 'cached_model'
|
54
|
+
|
55
|
+
Then edit your ActiveRecord model to inherit from CachedModel instead of
|
56
|
+
ActiveRecord::Base:
|
57
|
+
|
58
|
+
$ head -n 8 app/models/photo.rb
|
59
|
+
##
|
60
|
+
# A Photo from Flickr.
|
61
|
+
|
62
|
+
class Photo < CachedModel
|
63
|
+
|
64
|
+
belongs_to :point
|
65
|
+
belongs_to :route
|
66
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
require 'rake/gempackagetask'
|
6
|
+
|
7
|
+
$VERBOSE = nil
|
8
|
+
|
9
|
+
spec = Gem::Specification.new do |s|
|
10
|
+
s.name = 'cached_model'
|
11
|
+
s.version = '1.0.0'
|
12
|
+
s.summary = 'An ActiveRecord::Base model that caches records'
|
13
|
+
s.authors = 'Robert Cottrell, Eric Hodel'
|
14
|
+
s.email = 'bob@robotcoop.com'
|
15
|
+
|
16
|
+
s.has_rdoc = true
|
17
|
+
s.files = File.read('Manifest.txt').split($/)
|
18
|
+
s.require_path = 'lib'
|
19
|
+
|
20
|
+
s.add_dependency 'memcache-client', '1.0.3'
|
21
|
+
end
|
22
|
+
|
23
|
+
desc 'Run tests'
|
24
|
+
task :default => [ :test ]
|
25
|
+
|
26
|
+
Rake::TestTask.new('test') do |t|
|
27
|
+
t.libs << 'test'
|
28
|
+
t.pattern = 'test/test_*.rb'
|
29
|
+
t.verbose = true
|
30
|
+
end
|
31
|
+
|
32
|
+
desc 'Update Manifest.txt'
|
33
|
+
task :update_manifest do
|
34
|
+
sh "find . -type f | sed -e 's%./%%' | egrep -v 'svn|swp|~' | egrep -v '^(doc|pkg)/' | sort > Manifest.txt"
|
35
|
+
end
|
36
|
+
|
37
|
+
desc 'Generate RDoc'
|
38
|
+
Rake::RDocTask.new :rdoc do |rd|
|
39
|
+
rd.rdoc_dir = 'doc'
|
40
|
+
rd.rdoc_files.add 'lib', 'README', 'LICENSE'
|
41
|
+
rd.main = 'README'
|
42
|
+
rd.options << '-d' if `which dot` =~ /\/dot/
|
43
|
+
end
|
44
|
+
|
45
|
+
desc 'Build Gem'
|
46
|
+
Rake::GemPackageTask.new spec do |pkg|
|
47
|
+
pkg.need_tar = true
|
48
|
+
end
|
49
|
+
|
50
|
+
desc 'Clean up'
|
51
|
+
task :clean => [ :clobber_rdoc, :clobber_package ]
|
52
|
+
|
53
|
+
desc 'Clean up'
|
54
|
+
task :clobber => [ :clean ]
|
55
|
+
|
56
|
+
# vim: syntax=Ruby
|
57
|
+
|
data/lib/cached_model.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'memcache_util'
|
2
|
+
|
3
|
+
##
|
4
|
+
# An abstract ActiveRecord descendant that caches records in memcache and in
|
5
|
+
# local memory.
|
6
|
+
|
7
|
+
class CachedModel < ActiveRecord::Base
|
8
|
+
|
9
|
+
KEY = 'active_record'
|
10
|
+
TTL = 60 * 15 # seconds
|
11
|
+
|
12
|
+
@cache_local = {}
|
13
|
+
|
14
|
+
class << self; attr_reader :cache_local; end
|
15
|
+
|
16
|
+
##
|
17
|
+
# Override the flawed assumption ActiveRecord::Base makes about inheritance.
|
18
|
+
|
19
|
+
def self.descends_from_active_record?
|
20
|
+
superclass == CachedModel
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Override the flawed assumption ActiveRecord::Base makes about inheritance.
|
25
|
+
|
26
|
+
def self.class_name_of_active_record_descendant(klass)
|
27
|
+
if klass.superclass == CachedModel then
|
28
|
+
return klass.name
|
29
|
+
elsif klass.superclass.nil? then
|
30
|
+
raise ActiveRecordError, "#{name} doesn't descend from ActiveRecord::Base"
|
31
|
+
else
|
32
|
+
class_name_of_active_record_descendant klass.superclass
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Invalidate the cache entry for an record. The update method will
|
38
|
+
# automatically invalidate the cache when updates are made through
|
39
|
+
# ActiveRecord model record. However, several methods update tables with
|
40
|
+
# direct sql queries for effeciency. These methods should call this method
|
41
|
+
# to invalidate the cache after making those changes.
|
42
|
+
#
|
43
|
+
# NOTE - if a SQL query updates multiple rows with one query, there is
|
44
|
+
# currently no way to invalidate the affected entries unless the entire
|
45
|
+
# cache is dumped or until the TTL expires, so try not to do this.
|
46
|
+
|
47
|
+
def self.cache_delete(klass, id)
|
48
|
+
key = "#{klass}:#{id}"
|
49
|
+
CachedModel.cache_local.delete key
|
50
|
+
Cache.delete "#{KEY}:#{key}"
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Invalidate the per-request cache. This should be called from a before
|
55
|
+
# filter at the beginning of each request.
|
56
|
+
|
57
|
+
def self.cache_reset
|
58
|
+
CachedModel.cache_local.clear
|
59
|
+
end
|
60
|
+
|
61
|
+
##
|
62
|
+
# Override the find method to look for values in the cache before going to
|
63
|
+
# the database.
|
64
|
+
|
65
|
+
def self.find(*args)
|
66
|
+
args[0] = args.first.to_i if args.first =~ /\A\d+\Z/
|
67
|
+
# Only handle simple find requests. If the request was more complicated,
|
68
|
+
# let the base class handle it, but store the retrieved records in the
|
69
|
+
# local cache in case we need them later.
|
70
|
+
if $TESTING or args.length != 1 or not Fixnum === args.first then
|
71
|
+
records = super
|
72
|
+
if RAILS_ENV != 'test' and Array === records then
|
73
|
+
records.each { |r| r.cache_store }
|
74
|
+
end
|
75
|
+
return records
|
76
|
+
end
|
77
|
+
|
78
|
+
# Try to find the record in the local cache.
|
79
|
+
id = args.first
|
80
|
+
cache_key_local = "#{name}:#{id}"
|
81
|
+
record = CachedModel.cache_local[cache_key_local]
|
82
|
+
return record unless record.nil?
|
83
|
+
|
84
|
+
# Try to find the record in memcache and add it to the local cache
|
85
|
+
record = Cache.get "#{KEY}:#{cache_key_local}"
|
86
|
+
unless record.nil? then
|
87
|
+
CachedModel.cache_local[cache_key_local] = record
|
88
|
+
return record
|
89
|
+
end
|
90
|
+
|
91
|
+
# Fetch the record from the DB and cache it.
|
92
|
+
#
|
93
|
+
# We don't want the subsequent find_by_sql to loop back here, so guard
|
94
|
+
# the call.
|
95
|
+
#
|
96
|
+
# NOTE - this guard isn't thread safe.
|
97
|
+
begin
|
98
|
+
@skip_find_hack = true
|
99
|
+
record = super(args).first
|
100
|
+
record.cache_store
|
101
|
+
ensure
|
102
|
+
@skip_find_hack = false
|
103
|
+
end
|
104
|
+
|
105
|
+
return record
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Skip the special handling for find by primary key if this method was
|
110
|
+
# called from find. If this is really a lookup for a single row by primary
|
111
|
+
# key, use a simple find call instead.
|
112
|
+
|
113
|
+
def self.find_by_sql(*args)
|
114
|
+
unless @skip_find_hack or $TESTING then
|
115
|
+
if args.first =~ /SELECT \* FROM #{table_name} WHERE \(#{table_name}\.#{primary_key} = '?(\d+)'?\) LIMIT 1/ then
|
116
|
+
return [self.find($1.to_i)]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
return super
|
121
|
+
end
|
122
|
+
|
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
|
+
##
|
136
|
+
# Delete the entry from the cache now that it isn't in the DB.
|
137
|
+
|
138
|
+
def destroy
|
139
|
+
return super
|
140
|
+
ensure
|
141
|
+
cache_delete
|
142
|
+
end
|
143
|
+
|
144
|
+
##
|
145
|
+
# Invalidate the cache for this record before reloading from the DB.
|
146
|
+
|
147
|
+
def reload
|
148
|
+
cache_delete
|
149
|
+
return super
|
150
|
+
end
|
151
|
+
|
152
|
+
##
|
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.
|
161
|
+
|
162
|
+
def cache_store
|
163
|
+
cache_local[cache_key_memcache] = self
|
164
|
+
Cache.put cache_key_memcache, self, TTL
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Remove this record from the cache.
|
169
|
+
|
170
|
+
def cache_delete
|
171
|
+
cache_local.delete cache_key_local
|
172
|
+
Cache.delete cache_key_memcache
|
173
|
+
end
|
174
|
+
|
175
|
+
##
|
176
|
+
# The local cache key for this record.
|
177
|
+
|
178
|
+
def cache_key_local
|
179
|
+
return "#{self.class.name}:#{self.id}"
|
180
|
+
end
|
181
|
+
|
182
|
+
##
|
183
|
+
# The memcache key for this record.
|
184
|
+
|
185
|
+
def cache_key_memcache
|
186
|
+
return "#{KEY}:#{cache_key_local}"
|
187
|
+
end
|
188
|
+
|
189
|
+
end
|
190
|
+
|
metadata
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.8.11.6
|
3
|
+
specification_version: 1
|
4
|
+
name: cached_model
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 1.0.0
|
7
|
+
date: 2006-01-17 00:00:00 -08:00
|
8
|
+
summary: An ActiveRecord::Base model that caches records
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: bob@robotcoop.com
|
12
|
+
homepage:
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
authors:
|
29
|
+
- Robert Cottrell, Eric Hodel
|
30
|
+
files:
|
31
|
+
- Manifest.txt
|
32
|
+
- README
|
33
|
+
- Rakefile
|
34
|
+
- lib/cached_model.rb
|
35
|
+
test_files: []
|
36
|
+
|
37
|
+
rdoc_options: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
executables: []
|
42
|
+
|
43
|
+
extensions: []
|
44
|
+
|
45
|
+
requirements: []
|
46
|
+
|
47
|
+
dependencies:
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: memcache-client
|
50
|
+
version_requirement:
|
51
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
52
|
+
requirements:
|
53
|
+
- - "="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 1.0.3
|
56
|
+
version:
|