interlock 1.0 → 1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/Manifest +8 -15
- data/README +16 -73
- data/TODO +3 -0
- data/interlock.gemspec +16 -9
- data/lib/interlock/action_controller.rb +135 -36
- data/lib/interlock/action_view.rb +85 -18
- data/lib/interlock/active_record.rb +1 -1
- data/lib/interlock/config.rb +1 -1
- data/lib/interlock/interlock.rb +22 -3
- data/tasks/interlock.rake +10 -0
- data/test/integration/app/app/controllers/application.rb +2 -2
- data/test/integration/app/app/controllers/items_controller.rb +19 -1
- data/test/integration/app/app/views/items/detail.rhtml +20 -0
- data/test/integration/app/app/views/items/{list.html.erb → list.rhtml} +6 -0
- data/test/integration/app/app/views/items/{recent.html.erb → recent.rhtml} +0 -0
- data/test/integration/app/app/views/items/{show.html.erb → show.rhtml} +1 -1
- data/test/integration/app/app/views/layouts/application.html.erb +9 -0
- data/test/integration/app/app/views/shared/{_related.html.erb → _related.rhtml} +0 -0
- data/test/integration/app/config/boot.rb +2 -0
- data/test/integration/app/config/environment.rb +1 -3
- data/test/integration/server_test.rb +53 -6
- data/test/setup.rb +5 -0
- data/test/teardown.rb +0 -4
- data/test/test_helper.rb +9 -2
- data/test/unit/interlock_test.rb +21 -0
- data.tar.gz.sig +0 -0
- metadata +14 -20
- metadata.gz.sig +0 -0
- data/test/integration/app/coverage/cache-43041 +0 -0
- data/test/integration/app/coverage/index.html +0 -414
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_controller_rb.html +0 -733
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_view_rb.html +0 -644
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-active_record_rb.html +0 -639
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-config_rb.html +0 -688
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-core_extensions_rb.html +0 -674
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-interlock_rb.html +0 -722
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-memcached_rb.html +0 -640
- data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock_rb.html +0 -635
- data/test/integration/app/db/schema.rb +0 -21
data/CHANGELOG
CHANGED
data/Manifest
CHANGED
@@ -12,6 +12,7 @@ lib/interlock.rb
|
|
12
12
|
LICENSE
|
13
13
|
Manifest
|
14
14
|
README
|
15
|
+
tasks/interlock.rake
|
15
16
|
test/integration/app/app/controllers/application.rb
|
16
17
|
test/integration/app/app/controllers/eval_controller.rb
|
17
18
|
test/integration/app/app/controllers/items_controller.rb
|
@@ -19,10 +20,12 @@ test/integration/app/app/helpers/application_helper.rb
|
|
19
20
|
test/integration/app/app/helpers/eval_helper.rb
|
20
21
|
test/integration/app/app/helpers/items_helper.rb
|
21
22
|
test/integration/app/app/models/item.rb
|
22
|
-
test/integration/app/app/views/items/
|
23
|
-
test/integration/app/app/views/items/
|
24
|
-
test/integration/app/app/views/items/
|
25
|
-
test/integration/app/app/views/
|
23
|
+
test/integration/app/app/views/items/detail.rhtml
|
24
|
+
test/integration/app/app/views/items/list.rhtml
|
25
|
+
test/integration/app/app/views/items/recent.rhtml
|
26
|
+
test/integration/app/app/views/items/show.rhtml
|
27
|
+
test/integration/app/app/views/layouts/application.html.erb
|
28
|
+
test/integration/app/app/views/shared/_related.rhtml
|
26
29
|
test/integration/app/config/boot.rb
|
27
30
|
test/integration/app/config/database.yml
|
28
31
|
test/integration/app/config/environment.rb
|
@@ -33,18 +36,7 @@ test/integration/app/config/initializers/inflections.rb
|
|
33
36
|
test/integration/app/config/initializers/mime_types.rb
|
34
37
|
test/integration/app/config/memcached.yml
|
35
38
|
test/integration/app/config/routes.rb
|
36
|
-
test/integration/app/coverage/cache-43041
|
37
|
-
test/integration/app/coverage/index.html
|
38
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_controller_rb.html
|
39
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_view_rb.html
|
40
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-active_record_rb.html
|
41
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-config_rb.html
|
42
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-core_extensions_rb.html
|
43
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-interlock_rb.html
|
44
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-memcached_rb.html
|
45
|
-
test/integration/app/coverage/vendor-plugins-interlock-lib-interlock_rb.html
|
46
39
|
test/integration/app/db/migrate/001_create_items.rb
|
47
|
-
test/integration/app/db/schema.rb
|
48
40
|
test/integration/app/doc/README_FOR_APP
|
49
41
|
test/integration/app/public/404.html
|
50
42
|
test/integration/app/public/422.html
|
@@ -85,5 +77,6 @@ test/integration/server_test.rb
|
|
85
77
|
test/setup.rb
|
86
78
|
test/teardown.rb
|
87
79
|
test/test_helper.rb
|
80
|
+
test/unit/interlock_test.rb
|
88
81
|
test/unit/memcached_test.rb
|
89
82
|
TODO
|
data/README
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
|
2
2
|
Interlock
|
3
3
|
|
4
|
-
|
4
|
+
A Rails plugin for maintainable and high-efficiency caching.
|
5
5
|
|
6
6
|
== License
|
7
7
|
|
@@ -11,14 +11,17 @@ The public certificate for the gem is at http://rubyforge.org/frs/download.php/2
|
|
11
11
|
|
12
12
|
== Requirements
|
13
13
|
|
14
|
-
*
|
14
|
+
* memcached (http://www.danga.com/memcached)
|
15
15
|
* memcache-client gem
|
16
|
+
* Rails 1.2.6
|
17
|
+
|
18
|
+
Note that Rails 2.0.2 is required for caching <tt>content_for</tt> or nesting <tt>view_cache</tt> blocks.
|
16
19
|
|
17
20
|
== What it does
|
18
21
|
|
19
22
|
Interlock makes your view fragments and associated controller blocks march along together. If a fragment is fresh, the controller behavior won't run. This eliminates duplicate effort from your request cycle. Your controller blocks run so infrequently that you can use regular ActiveRecord finders and not worry about object caching at all.
|
20
23
|
|
21
|
-
Interlock automatically tracks invalidation dependencies based on the model lifecyle, and supports arbitrary levels of
|
24
|
+
Interlock automatically tracks invalidation dependencies based on the model lifecyle, and supports arbitrary levels of scoping per-block. It also caches <tt>content_for</tt> calls, unlike regular Rails.
|
22
25
|
|
23
26
|
== Installation
|
24
27
|
|
@@ -47,7 +50,7 @@ Now you're ready to go.
|
|
47
50
|
|
48
51
|
== Usage
|
49
52
|
|
50
|
-
Interlock provides two similar caching methods: <tt>behavior_cache</tt> for controllers and <tt>view_cache</tt> for views.
|
53
|
+
Interlock provides two similar caching methods: <tt>behavior_cache</tt> for controllers and <tt>view_cache</tt> for views. They both accept an optional list or hash of model dependencies, and an optional <tt>:tag</tt> keypair. <tt>view_cache</tt> also accepts a <tt>:ttl</tt> keypair.
|
51
54
|
|
52
55
|
The simplest usage doesn't require any parameters. In the controller:
|
53
56
|
|
@@ -61,7 +64,7 @@ The simplest usage doesn't require any parameters. In the controller:
|
|
61
64
|
|
62
65
|
end
|
63
66
|
|
64
|
-
Now, in the view, wrap the largest section of ERB you can find that uses data from <tt>@items</tt>
|
67
|
+
Now, in the view, wrap the largest section of ERB you can find that uses data from <tt>@items</tt> in a <tt>view_cache</tt> block. No other part of the view can refer to <tt>@items</tt>, because <tt>@items</tt> won't get set unless the cache is stale.
|
65
68
|
|
66
69
|
<% @title = "My Sweet Items" %>
|
67
70
|
|
@@ -77,74 +80,9 @@ This automatically registers a caching dependency on Item for <tt>slow_action</t
|
|
77
80
|
|
78
81
|
You can use multiple instance variables in one block, of course. Just make sure the <tt>behavior_cache</tt> provides whatever the <tt>view_cache</tt> uses.
|
79
82
|
|
80
|
-
|
81
|
-
|
82
|
-
You can declare non-default invalidation dependencies by passing models to <tt>behavior_cache</tt> (you can also pass them to <tt>view_cache</tt>, but you should only do that if you are caching a fragment without an associated behavior block in the controller).
|
83
|
-
|
84
|
-
<b>No dependencies (cache never invalidates):</b>
|
85
|
-
behavior_cache nil do
|
86
|
-
end
|
87
|
-
|
88
|
-
<b>Invalidate on any Media change:</b>
|
89
|
-
behavior_cache Media do
|
90
|
-
end
|
91
|
-
|
92
|
-
<b>Invalidate on any Media or Item change:</b>
|
93
|
-
behavior_cache Media, Item do
|
94
|
-
end
|
95
|
-
|
96
|
-
<b>Invalidate on Item changes if the Item <tt>id</tt> matches the current <tt>params[:id]</tt> value:</b>
|
97
|
-
behavior_cache Item => :id do
|
98
|
-
end
|
99
|
-
|
100
|
-
You do not have to pass the same dependencies to <tt>behavior_cache</tt> and <tt>view_cache</tt> even for the same action. The set union of both dependency lists will be used.
|
101
|
-
|
102
|
-
== Narrowing scope and caching multiple blocks
|
103
|
-
|
104
|
-
Sometimes you need to cache multiple blocks in a controller, or otherwise get a more fine-grained scope. Interlock provides the <tt>:tag</tt> key for this purpose. <tt>:tag</tt> accepts either an array of symbols, which are mapped to <tt>params</tt> values, or an arbitrary object, which is converted to a string identifier. <b>Your corresponding behavior caches and view caches must have identical <tt>:tag</tt> values for the interlocking to take effect.</b>
|
105
|
-
|
106
|
-
Note that <tt>:tag</tt> can be used to scope caches. You can simultaneously cache different versions of the same block, differentiating based on params or other logic. This is great for caching per-user, for example:
|
107
|
-
|
108
|
-
def profile
|
109
|
-
@user = current_user
|
110
|
-
behavior_cache :tag => @user do
|
111
|
-
@items = Item.find(:all, :conditions => "be slow")
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
In the view, use the same <tt>:tag</tt> value (<tt>@user</tt>). Note that <tt>@user</tt> must be set outside of the behavior block in the controller, because its contents are used to decide whether to run the block in the first place.
|
116
|
-
|
117
|
-
This way each user will see only their own cache. Pretty neat.
|
118
|
-
|
119
|
-
== Broadening scope
|
120
|
-
|
121
|
-
Sometimes the default scope (<tt>controller</tt>, <tt>action</tt>, <tt>params[:id]</tt>) is too narrow. For example, you might share a partial across actions, and set up its data via a filter. By default, Interlock will cache a separate version of it for each action. To avoid this, you can use the <tt>:ignore</tt> key, which lets you list parts of the default scope to ignore:
|
122
|
-
|
123
|
-
before_filter :recent
|
124
|
-
|
125
|
-
private
|
126
|
-
|
127
|
-
def recent
|
128
|
-
behavior_cache :ignore => :action do
|
129
|
-
@recent = Item.find(:all, :limit => 5, :order => 'updated_at DESC')
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
Valid values for <tt>:ignore</tt> are <tt>:controller</tt>, <tt>:action</tt>, <tt>:id</tt>, and <tt>:all</tt>. You can pass an array of multiple values. <b>Just like with <tt>:tag</tt>, your corresponding behavior caches and view caches must have identical <tt>:ignore</tt> values.</b> Note that cache blocks with <tt>:ignore</tt> values still obey the regular invalidation rules.
|
134
|
-
|
135
|
-
A good way to get started is to just use the default scope. Then <tt>grep</tt> in the production log for <tt>interlock</tt> and see what keys are being set and read. If you see lots of different keys go by for data that you know is the same, then set some <tt>:ignore</tt> values.
|
83
|
+
See ActionController::Base and ActionView::Helpers::CacheHelper for more details.
|
136
84
|
|
137
|
-
==
|
138
|
-
|
139
|
-
It's fine to use a <tt>view_cache</tt> block without a <tt>behavior_cache</tt> block. For example, to mimic regular fragment cache behavior, but take advantage of Memcached's <tt>:ttl</tt> support, call:
|
140
|
-
|
141
|
-
<% view_cache nil, :ignore => :all, :tag => 'sidebar', :ttl => 5.minutes %>
|
142
|
-
<h1>On the side</h1>
|
143
|
-
<% end %>
|
144
|
-
|
145
|
-
Remember that <tt>nil</tt> disables invalidation rules. This is a nice trick for keeping your caching strategy unified.
|
146
|
-
|
147
|
-
== Gotchas
|
85
|
+
== Notes
|
148
86
|
|
149
87
|
You will not see any actual cache reuse in development mode unless you set <tt>config.action_controller.perform_caching = true</tt> in <tt>config/environments/development.rb</tt>.
|
150
88
|
|
@@ -161,8 +99,13 @@ You can write custom invalidation rules if you really want to, but try hard to a
|
|
161
99
|
|
162
100
|
Also, Interlock obeys the <tt>ENV['RAILS_ASSET_ID']</tt> setting, so if you need to blanket-invalidate all your caches, just change <tt>RAILS_ASSET_ID</tt> (for example, you could have it increment on every deploy).
|
163
101
|
|
102
|
+
== Further resources
|
103
|
+
|
104
|
+
* http://blog.evanweaver.com/articles/2007/12/13/better-rails-caching/
|
105
|
+
* http://www.socialtext.net/memcached/index.cgi?faq
|
106
|
+
|
164
107
|
== Reporting problems
|
165
108
|
|
166
|
-
* http://rubyforge.org/forum/forum.php?forum_id=
|
109
|
+
* http://rubyforge.org/forum/forum.php?forum_id=19835
|
167
110
|
|
168
111
|
Patches and contributions are very welcome. Please note that contributors are required to assign copyright for their additions to Cloudburst, LLC.
|
data/TODO
CHANGED
data/interlock.gemspec
CHANGED
@@ -1,26 +1,26 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Interlock-1.
|
2
|
+
# Gem::Specification for Interlock-1.1
|
3
3
|
# Originally generated by Echoe
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = %q{interlock}
|
7
|
-
s.version = "1.
|
7
|
+
s.version = "1.1"
|
8
8
|
|
9
9
|
s.specification_version = 2 if s.respond_to? :specification_version=
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.authors = [""]
|
13
|
-
s.date = %q{
|
14
|
-
s.description = %q{
|
13
|
+
s.date = %q{2008-01-15}
|
14
|
+
s.description = %q{A Rails plugin for maintainable and high-efficiency caching.}
|
15
15
|
s.email = %q{}
|
16
|
-
s.files = ["CHANGELOG", "examples/memcached.yml", "init.rb", "lib/interlock/action_controller.rb", "lib/interlock/action_view.rb", "lib/interlock/active_record.rb", "lib/interlock/config.rb", "lib/interlock/core_extensions.rb", "lib/interlock/interlock.rb", "lib/interlock/memcached.rb", "lib/interlock.rb", "LICENSE", "Manifest", "README", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/eval_controller.rb", "test/integration/app/app/controllers/items_controller.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/eval_helper.rb", "test/integration/app/app/helpers/items_helper.rb", "test/integration/app/app/models/item.rb", "test/integration/app/app/views/items/list.
|
16
|
+
s.files = ["CHANGELOG", "examples/memcached.yml", "init.rb", "lib/interlock/action_controller.rb", "lib/interlock/action_view.rb", "lib/interlock/active_record.rb", "lib/interlock/config.rb", "lib/interlock/core_extensions.rb", "lib/interlock/interlock.rb", "lib/interlock/memcached.rb", "lib/interlock.rb", "LICENSE", "Manifest", "README", "tasks/interlock.rake", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/eval_controller.rb", "test/integration/app/app/controllers/items_controller.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/eval_helper.rb", "test/integration/app/app/helpers/items_helper.rb", "test/integration/app/app/models/item.rb", "test/integration/app/app/views/items/detail.rhtml", "test/integration/app/app/views/items/list.rhtml", "test/integration/app/app/views/items/recent.rhtml", "test/integration/app/app/views/items/show.rhtml", "test/integration/app/app/views/layouts/application.html.erb", "test/integration/app/app/views/shared/_related.rhtml", "test/integration/app/config/boot.rb", "test/integration/app/config/database.yml", "test/integration/app/config/environment.rb", "test/integration/app/config/environments/development.rb", "test/integration/app/config/environments/production.rb", "test/integration/app/config/environments/test.rb", "test/integration/app/config/initializers/inflections.rb", "test/integration/app/config/initializers/mime_types.rb", "test/integration/app/config/memcached.yml", "test/integration/app/config/routes.rb", "test/integration/app/db/migrate/001_create_items.rb", "test/integration/app/doc/README_FOR_APP", "test/integration/app/public/404.html", "test/integration/app/public/422.html", "test/integration/app/public/500.html", "test/integration/app/public/dispatch.cgi", "test/integration/app/public/dispatch.fcgi", "test/integration/app/public/dispatch.rb", "test/integration/app/public/favicon.ico", "test/integration/app/public/images/rails.png", "test/integration/app/public/index.html", "test/integration/app/public/javascripts/application.js", "test/integration/app/public/javascripts/controls.js", "test/integration/app/public/javascripts/dragdrop.js", "test/integration/app/public/javascripts/effects.js", "test/integration/app/public/javascripts/prototype.js", "test/integration/app/public/robots.txt", "test/integration/app/Rakefile", "test/integration/app/README", "test/integration/app/script/about", "test/integration/app/script/console", "test/integration/app/script/destroy", "test/integration/app/script/generate", "test/integration/app/script/performance/benchmarker", "test/integration/app/script/performance/profiler", "test/integration/app/script/performance/request", "test/integration/app/script/plugin", "test/integration/app/script/process/inspector", "test/integration/app/script/process/reaper", "test/integration/app/script/process/spawner", "test/integration/app/script/runner", "test/integration/app/script/server", "test/integration/app/test/fixtures/items.yml", "test/integration/app/test/functional/eval_controller_test.rb", "test/integration/app/test/functional/items_controller_test.rb", "test/integration/app/test/test_helper.rb", "test/integration/app/test/unit/item_test.rb", "test/integration/server_test.rb", "test/setup.rb", "test/teardown.rb", "test/test_helper.rb", "test/unit/interlock_test.rb", "test/unit/memcached_test.rb", "TODO", "interlock.gemspec"]
|
17
17
|
s.has_rdoc = true
|
18
18
|
s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/interlock/}
|
19
19
|
s.require_paths = ["lib"]
|
20
20
|
s.rubyforge_project = %q{fauna}
|
21
|
-
s.rubygems_version = %q{0.
|
22
|
-
s.summary = %q{
|
23
|
-
s.test_files = ["test/integration/server_test.rb", "test/unit/memcached_test.rb"]
|
21
|
+
s.rubygems_version = %q{1.0.1}
|
22
|
+
s.summary = %q{A Rails plugin for maintainable and high-efficiency caching.}
|
23
|
+
s.test_files = ["test/integration/server_test.rb", "test/unit/interlock_test.rb", "test/unit/memcached_test.rb"]
|
24
24
|
|
25
25
|
s.add_dependency(%q<memcache_client>, [">= 1.5.0"])
|
26
26
|
end
|
@@ -33,12 +33,14 @@ end
|
|
33
33
|
#
|
34
34
|
# Echoe.new("interlock") do |p|
|
35
35
|
# p.project = "fauna"
|
36
|
-
# p.summary = "
|
36
|
+
# p.summary = "A Rails plugin for maintainable and high-efficiency caching."
|
37
37
|
# p.url = "http://blog.evanweaver.com/files/doc/fauna/interlock/"
|
38
38
|
# p.docs_host = "blog.evanweaver.com:~/www/bax/public/files/doc/"
|
39
39
|
# p.dependencies = "memcache_client >=1.5.0"
|
40
40
|
# p.test_pattern = ["test/integration/*.rb", "test/unit/*.rb"]
|
41
41
|
# p.rdoc_pattern = ["README", "CHANGELOG", "TODO", "LICENSE", "lib/interlock/memcached.rb", "lib/interlock/interlock.rb", "lib/interlock/action_controller.rb", "lib/interlock/active_record.rb", "lib/interlock/action_view.rb", "lib/interlock/config.rb"]
|
42
|
+
# p.clean_pattern += ['test/integration/app/coverage', 'test/integration/app/db/schema.rb',
|
43
|
+
# 'test/integration/app/vendor/plugins/interlock']
|
42
44
|
# end
|
43
45
|
#
|
44
46
|
# desc "Run all the tests in production and development mode both"
|
@@ -50,3 +52,8 @@ end
|
|
50
52
|
# STDERR.puts "#{'='*80}\nProduction mode\n#{'='*80}"
|
51
53
|
# system("rake test:multi_rails:all")
|
52
54
|
# end
|
55
|
+
#
|
56
|
+
# task "tail" do
|
57
|
+
# log = "test/integration/app/log/development.log"
|
58
|
+
# system("touch #{log} && tail -f #{log} | grep interlock")
|
59
|
+
# end
|
@@ -2,20 +2,15 @@
|
|
2
2
|
module ActionController #:nodoc:
|
3
3
|
class Base
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
#
|
15
|
-
# If you pass an Array of symbols as the tag, it will get value-mapped onto
|
16
|
-
# params and sorted. This makes granular scoping easier, although it doesn't
|
17
|
-
# sidestep the normal blanket invalidations.
|
18
|
-
#
|
5
|
+
|
6
|
+
=begin rdoc
|
7
|
+
Build the fragment key from a particular context. This must be deterministic and stateful except for the tag. We can't scope the key to arbitrary params because the view doesn't have access to which are relevant and which are not.
|
8
|
+
|
9
|
+
Note that the tag can be pretty much any object. Define <tt>to_interlock_tag</tt> if you need custom tagging for some class. ActiveRecord::Base already has it defined appropriately.
|
10
|
+
|
11
|
+
If you pass an Array of symbols as the tag, it will get value-mapped onto params and sorted. This makes granular scoping easier, although it doesn't sidestep the normal blanket invalidations.
|
12
|
+
=end
|
13
|
+
|
19
14
|
def caching_key(ignore = nil, tag = nil)
|
20
15
|
ignore = Array(ignore)
|
21
16
|
ignore = Interlock::SCOPE_KEYS if ignore.include? :all
|
@@ -40,23 +35,107 @@ module ActionController #:nodoc:
|
|
40
35
|
)
|
41
36
|
end
|
42
37
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
38
|
+
|
39
|
+
=begin rdoc
|
40
|
+
|
41
|
+
<tt>behavior_cache</tt> marks a controller block for caching. It accepts a list of class dependencies for invalidation, as well as as <tt>:tag</tt> and <tt>:ignore</tt> keys for explicit fragment scoping. It does not accept a <tt>:ttl</tt> key.
|
42
|
+
|
43
|
+
Please note that the behavior of nested <tt>behavior_cache</tt> blocks is undefined.
|
44
|
+
|
45
|
+
== Declaring dependencies
|
46
|
+
|
47
|
+
You can declare non-default invalidation dependencies by passing models to <tt>behavior_cache</tt> (you can also pass them to <tt>view_cache</tt>, but you should only do that if you are caching a fragment without an associated behavior block in the controller).
|
48
|
+
|
49
|
+
<b>No dependencies (cache never invalidates):</b>
|
50
|
+
behavior_cache nil do
|
51
|
+
end
|
52
|
+
|
53
|
+
<b>Invalidate on any Media change:</b>
|
54
|
+
behavior_cache Media do
|
55
|
+
end
|
56
|
+
|
57
|
+
<b>Invalidate on any Media or Item change:</b>
|
58
|
+
behavior_cache Media, Item do
|
59
|
+
end
|
60
|
+
|
61
|
+
<b>Invalidate on Item changes if the Item <tt>id</tt> matches the current <tt>params[:id]</tt> value:</b>
|
62
|
+
behavior_cache Item => :id do
|
63
|
+
end
|
64
|
+
|
65
|
+
You do not have to pass the same dependencies to <tt>behavior_cache</tt> and <tt>view_cache</tt> even for the same action. The set union of both dependency lists will be used.
|
66
|
+
|
67
|
+
== Narrowing scope and caching multiple blocks
|
68
|
+
|
69
|
+
Sometimes you need to cache multiple blocks in a controller, or otherwise get a more fine-grained scope. Interlock provides the <tt>:tag</tt> key for this purpose. <tt>:tag</tt> accepts either an array of symbols, which are mapped to <tt>params</tt> values, or an arbitrary object, which is converted to a string identifier. <b>Your corresponding behavior caches and view caches must have identical <tt>:tag</tt> values for the interlocking to take effect.</b>
|
70
|
+
|
71
|
+
Note that <tt>:tag</tt> can be used to scope caches. You can simultaneously cache different versions of the same block, differentiating based on params or other logic. This is great for caching per-user, for example:
|
72
|
+
|
73
|
+
def profile
|
74
|
+
@user = current_user
|
75
|
+
behavior_cache :tag => @user do
|
76
|
+
@items = @user.items
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
In the view, use the same <tt>:tag</tt> value (<tt>@user</tt>). Note that <tt>@user</tt> must be set outside of the behavior block in the controller, because its contents are used to decide whether to run the block in the first place.
|
81
|
+
|
82
|
+
This way each user will see only their own cache. Pretty neat.
|
83
|
+
|
84
|
+
== Broadening scope
|
85
|
+
|
86
|
+
Sometimes the default scope (<tt>controller</tt>, <tt>action</tt>, <tt>params[:id]</tt>) is too narrow. For example, you might share a partial across actions, and set up its data via a filter. By default, Interlock will cache a separate version of it for each action. To avoid this, you can use the <tt>:ignore</tt> key, which lets you list parts of the default scope to ignore:
|
87
|
+
|
88
|
+
before_filter :recent
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
def recent
|
93
|
+
behavior_cache :ignore => :action do
|
94
|
+
@recent = Item.find(:all, :limit => 5, :order => 'updated_at DESC')
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
Valid values for <tt>:ignore</tt> are <tt>:controller</tt>, <tt>:action</tt>, <tt>:id</tt>, and <tt>:all</tt>. You can pass an array of multiple values. <b>Just like with <tt>:tag</tt>, your corresponding behavior caches and view caches must have identical <tt>:ignore</tt> values.</b> Note that cache blocks with <tt>:ignore</tt> values still obey the regular invalidation rules.
|
99
|
+
|
100
|
+
A good way to get started is to just use the default scope. Then <tt>grep</tt> in the production log for <tt>interlock</tt> and see what keys are being set and read. If you see lots of different keys go by for data that you know is the same, then set some <tt>:ignore</tt> values.
|
101
|
+
|
102
|
+
== Skipping caching
|
103
|
+
|
104
|
+
You can pass <tt>:perform => false</tt> to disable caching, for example, in a preview action. Note that <tt>:perform</tt> only responds to <tt>false</tt>, not <tt>nil</tt>. This allows for handier view reuse because you can set <tt>:perform</tt> to an instance variable and it will still cache if the instance variable is not set:
|
105
|
+
|
106
|
+
def preview
|
107
|
+
@perform = false
|
108
|
+
behavior_cache :perform => @perform do
|
109
|
+
end
|
110
|
+
render :action => 'show'
|
111
|
+
end
|
112
|
+
|
113
|
+
And in the <tt>show.html.erb</tt> view:
|
114
|
+
|
115
|
+
<% view_cache :perform => @perform do %>
|
116
|
+
<% end %>
|
117
|
+
|
118
|
+
=end
|
119
|
+
|
47
120
|
def behavior_cache(*args)
|
48
121
|
conventional_class = begin; controller_name.classify.constantize; rescue NameError; end
|
49
122
|
options, dependencies = Interlock.extract_options_and_dependencies(args, conventional_class)
|
50
123
|
|
51
124
|
raise UsageError, ":ttl has no effect in a behavior_cache block" if options[:ttl]
|
52
|
-
|
125
|
+
|
53
126
|
key = caching_key(options.value_for_indifferent_key(:ignore), options.value_for_indifferent_key(:tag))
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
unless read_fragment(key)
|
58
|
-
Interlock.say key, "is running the controller block"
|
127
|
+
|
128
|
+
if options[:perform] == false
|
129
|
+
Interlock.say key, "is not cached"
|
59
130
|
yield
|
131
|
+
else
|
132
|
+
Interlock.register_dependencies(dependencies, key)
|
133
|
+
|
134
|
+
# See if the fragment exists, and run the block if it doesn't.
|
135
|
+
unless read_fragment(key, :assign_content_for => false)
|
136
|
+
Interlock.say key, "is running the controller block"
|
137
|
+
yield
|
138
|
+
end
|
60
139
|
end
|
61
140
|
end
|
62
141
|
|
@@ -70,7 +149,7 @@ module ActionController #:nodoc:
|
|
70
149
|
# Callback to reset the local cache.
|
71
150
|
#
|
72
151
|
def clear_interlock_local_cache
|
73
|
-
Interlock.local_cache = ActionController::Base::MemoryStore.new
|
152
|
+
Interlock.local_cache = ::ActionController::Base::MemoryStore.new
|
74
153
|
RAILS_DEFAULT_LOGGER.warn "** cleared interlock local cache"
|
75
154
|
end
|
76
155
|
|
@@ -83,39 +162,59 @@ module ActionController #:nodoc:
|
|
83
162
|
module Fragments
|
84
163
|
|
85
164
|
#
|
86
|
-
# Replaces Rail's write_fragment method. Avoids extra checks for regex keys
|
87
|
-
# which are unsupported, adds more detailed logging information,
|
88
|
-
#
|
165
|
+
# Replaces Rail's write_fragment method. Avoids extra checks for regex keys
|
166
|
+
# which are unsupported, adds more detailed logging information, stores writes
|
167
|
+
# in the local process cache too to avoid duplicate memcached requests, and
|
168
|
+
# includes the content_for cache in the fragment.
|
89
169
|
#
|
90
|
-
def write_fragment(key,
|
170
|
+
def write_fragment(key, block_content, options = nil)
|
91
171
|
return unless perform_caching
|
172
|
+
|
173
|
+
content = [block_content, @template.cached_content_for]
|
92
174
|
|
93
175
|
fragment_cache_store.write(key, content, options)
|
94
176
|
Interlock.local_cache.write(key, content, options)
|
95
177
|
|
96
178
|
Interlock.say key, "wrote"
|
97
179
|
|
98
|
-
|
180
|
+
block_content
|
99
181
|
end
|
100
182
|
|
101
183
|
#
|
102
184
|
# Replaces Rail's read_fragment method. Avoids checks for regex keys,
|
103
|
-
# which are unsupported, adds more detailed logging information,
|
104
|
-
#
|
105
|
-
# memcached are
|
185
|
+
# which are unsupported, adds more detailed logging information, checks
|
186
|
+
# the local process cache before hitting memcached, and restores the
|
187
|
+
# content_for cache. Hits on memcached are also stored back locally to
|
188
|
+
# avoid duplicate requests.
|
106
189
|
#
|
107
190
|
def read_fragment(key, options = nil)
|
108
191
|
return unless perform_caching
|
109
192
|
|
110
193
|
if content = Interlock.local_cache.read(key, options)
|
111
194
|
# Interlock.say key, "read from local cache"
|
112
|
-
elsif content = fragment_cache_store.read(key, options)
|
195
|
+
elsif content = fragment_cache_store.read(key, options)
|
196
|
+
raise FragmentConsistencyError, "#{key} is not an Array" unless content.is_a? Array
|
113
197
|
Interlock.say key, "read from memcached"
|
114
198
|
Interlock.local_cache.write(key, content, options)
|
115
199
|
else
|
116
|
-
#
|
200
|
+
# Not found
|
201
|
+
return nil
|
117
202
|
end
|
118
|
-
|
203
|
+
|
204
|
+
raise FragmentConsistencyError, "#{key}::content is not a String" unless content.first.is_a? String
|
205
|
+
|
206
|
+
options ||= {}
|
207
|
+
# Note that 'nil' is considered true for :assign_content_for
|
208
|
+
if options[:assign_content_for] != false and content.last
|
209
|
+
# Extract content_for variables
|
210
|
+
content.last.each do |name, value|
|
211
|
+
raise FragmentConsistencyError, "#{key}::content_for(:#{name}) is not a String" unless value.is_a? String
|
212
|
+
# We'll just call the helper because that will handle nested view_caches properly.
|
213
|
+
@template.send(:content_for, name, value)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
content.first
|
119
218
|
end
|
120
219
|
|
121
220
|
end
|
@@ -1,28 +1,75 @@
|
|
1
1
|
|
2
2
|
module ActionView #:nodoc:
|
3
|
+
class Base #:nodoc:
|
4
|
+
attr_accessor :cached_content_for
|
5
|
+
end
|
6
|
+
|
3
7
|
module Helpers #:nodoc:
|
4
8
|
module CacheHelper
|
5
9
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
=begin rdoc
|
11
|
+
|
12
|
+
<tt>view_cache</tt> marks a corresponding view block for caching. It accepts <tt>:tag</tt> and <tt>:ignore</tt> keys for explicit scoping, as well as a <tt>:ttl</tt> key and a <tt>:perform</tt> key.
|
13
|
+
|
14
|
+
You can specify dependencies in <tt>view_cache</tt> if you really want to. Note that unlike <tt>behavior_cache</tt>, <tt>view_cache</tt> doesn't set up any default dependencies.
|
15
|
+
|
16
|
+
Nested <tt>view_cache</tt> blocks work fine. You would only need to nest if you had a slowly invalidating block contained in a more quickly invalidating block; otherwise there's no benefit.
|
17
|
+
|
18
|
+
Finally, caching <tt>content_for</tt> within a <tt>view_cache</tt> works, unlike regular Rails. It even works in nested caches.
|
19
|
+
|
20
|
+
== Setting a TTL
|
21
|
+
|
22
|
+
Use the <tt>:ttl</tt> key to specify a maximum time-to-live, in seconds:
|
23
|
+
|
24
|
+
<% view_cache :ttl => 5.minutes do %>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
Note that the cached item is not guaranteed to live this long. An invalidation rule could trigger first, or memcached could eject the item early due to the LRU.
|
28
|
+
|
29
|
+
== View caching without action caching
|
30
|
+
|
31
|
+
It's fine to use a <tt>view_cache</tt> block without a <tt>behavior_cache</tt> block. For example, to mimic regular fragment cache behavior, but take advantage of memcached's <tt>:ttl</tt> support, call:
|
32
|
+
|
33
|
+
<% view_cache :ignore => :all, :tag => 'sidebar', :ttl => 5.minutes %>
|
34
|
+
<% end %>
|
35
|
+
|
36
|
+
== Dependencies, scoping, and other options
|
37
|
+
|
38
|
+
See ActionController::Base for explanations of the rest of the options. The <tt>view_cache</tt> and <tt>behavior_cache</tt> APIs are identical except for setting the <tt>:ttl</tt>, which can only be done in the view, and the default dependency, which is only set by <tt>behavior_cache</tt>.
|
39
|
+
|
40
|
+
=end
|
41
|
+
def view_cache(*args, &block)
|
42
|
+
# conventional_class = begin; controller.controller_name.classify.constantize; rescue NameError; end
|
43
|
+
options, dependencies = Interlock.extract_options_and_dependencies(args, nil)
|
44
|
+
|
16
45
|
key = controller.caching_key(options.value_for_indifferent_key(:ignore), options.value_for_indifferent_key(:tag))
|
17
|
-
Interlock.register_dependencies(dependencies, key)
|
18
46
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
47
|
+
if options[:perform] == false
|
48
|
+
# Interlock.say key, "is not cached"
|
49
|
+
block.call
|
50
|
+
else
|
51
|
+
Interlock.register_dependencies(dependencies, key)
|
52
|
+
|
53
|
+
# Interlock.say key, "is rendering"
|
54
|
+
|
55
|
+
@cached_content_for, previous_cached_content_for = {}, @cached_content_for
|
56
|
+
@controller.cache_erb_fragment(
|
57
|
+
block,
|
58
|
+
key,
|
59
|
+
:ttl => (options.value_for_indifferent_key(:ttl) or Interlock.config[:ttl])
|
60
|
+
)
|
61
|
+
|
62
|
+
# This is tricky. If we were already caching content_fors in a parent block, we need to
|
63
|
+
# append the content_fors set in the inner block to those already set in the outer block.
|
64
|
+
if previous_cached_content_for
|
65
|
+
@cached_content_for.each do |key, value|
|
66
|
+
previous_cached_content_for[key] = "#{previous_cached_content_for[key]}#{value}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Restore the cache state
|
71
|
+
@cached_content_for = previous_cached_content_for
|
72
|
+
end
|
26
73
|
end
|
27
74
|
|
28
75
|
#:stopdoc:
|
@@ -30,5 +77,25 @@ module ActionView #:nodoc:
|
|
30
77
|
#:startdoc:
|
31
78
|
|
32
79
|
end
|
80
|
+
|
81
|
+
|
82
|
+
module CaptureHelper
|
83
|
+
#
|
84
|
+
# Override content_for so we can cache the instance variables it sets along with the fragment.
|
85
|
+
#
|
86
|
+
def content_for(name, content = nil, &block)
|
87
|
+
ivar = "@content_for_#{name}"
|
88
|
+
existing_content = instance_variable_get(ivar).to_s
|
89
|
+
this_content = (block_given? ? capture(&block) : content)
|
90
|
+
|
91
|
+
# If we are in a view_cache block, cache what we added to this instance variable
|
92
|
+
if @cached_content_for
|
93
|
+
@cached_content_for[name] = "#{@cached_content_for[name]}#{this_content}"
|
94
|
+
end
|
95
|
+
|
96
|
+
instance_variable_set(ivar, existing_content + this_content)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
33
100
|
end
|
34
101
|
end
|
@@ -14,7 +14,7 @@ module ActiveRecord #:nodoc:
|
|
14
14
|
#
|
15
15
|
|
16
16
|
def expire_interlock_keys
|
17
|
-
(CACHE.get(Interlock.dependency_key(self.class)) || {}).each do |key, scope|
|
17
|
+
(CACHE.get(Interlock.dependency_key(self.class.base_class)) || {}).each do |key, scope|
|
18
18
|
if scope == :all or (scope == :id and key.field(4) == self.to_param.to_s)
|
19
19
|
Interlock.say key, "invalidated by rule #{self.class} -> #{scope.inspect}."
|
20
20
|
Interlock.invalidate key
|