interlock 1.0 → 1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. data/CHANGELOG +2 -0
  2. data/Manifest +8 -15
  3. data/README +16 -73
  4. data/TODO +3 -0
  5. data/interlock.gemspec +16 -9
  6. data/lib/interlock/action_controller.rb +135 -36
  7. data/lib/interlock/action_view.rb +85 -18
  8. data/lib/interlock/active_record.rb +1 -1
  9. data/lib/interlock/config.rb +1 -1
  10. data/lib/interlock/interlock.rb +22 -3
  11. data/tasks/interlock.rake +10 -0
  12. data/test/integration/app/app/controllers/application.rb +2 -2
  13. data/test/integration/app/app/controllers/items_controller.rb +19 -1
  14. data/test/integration/app/app/views/items/detail.rhtml +20 -0
  15. data/test/integration/app/app/views/items/{list.html.erb → list.rhtml} +6 -0
  16. data/test/integration/app/app/views/items/{recent.html.erb → recent.rhtml} +0 -0
  17. data/test/integration/app/app/views/items/{show.html.erb → show.rhtml} +1 -1
  18. data/test/integration/app/app/views/layouts/application.html.erb +9 -0
  19. data/test/integration/app/app/views/shared/{_related.html.erb → _related.rhtml} +0 -0
  20. data/test/integration/app/config/boot.rb +2 -0
  21. data/test/integration/app/config/environment.rb +1 -3
  22. data/test/integration/server_test.rb +53 -6
  23. data/test/setup.rb +5 -0
  24. data/test/teardown.rb +0 -4
  25. data/test/test_helper.rb +9 -2
  26. data/test/unit/interlock_test.rb +21 -0
  27. data.tar.gz.sig +0 -0
  28. metadata +14 -20
  29. metadata.gz.sig +0 -0
  30. data/test/integration/app/coverage/cache-43041 +0 -0
  31. data/test/integration/app/coverage/index.html +0 -414
  32. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_controller_rb.html +0 -733
  33. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_view_rb.html +0 -644
  34. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-active_record_rb.html +0 -639
  35. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-config_rb.html +0 -688
  36. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-core_extensions_rb.html +0 -674
  37. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-interlock_rb.html +0 -722
  38. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-memcached_rb.html +0 -640
  39. data/test/integration/app/coverage/vendor-plugins-interlock-lib-interlock_rb.html +0 -635
  40. data/test/integration/app/db/schema.rb +0 -21
data/CHANGELOG CHANGED
@@ -1,2 +1,4 @@
1
1
 
2
+ v1.1. Add :perform => false option. Cache :content_for. Test nested view_cache blocks. Don't assign conventional dependency when called from the view, because we don't know what the controller might have already specified.
3
+
2
4
  v1.0. First release.
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/list.html.erb
23
- test/integration/app/app/views/items/recent.html.erb
24
- test/integration/app/app/views/items/show.html.erb
25
- test/integration/app/app/views/shared/_related.html.erb
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
- An optimal-efficiency caching plugin for Rails.
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
- * Memcached (http://www.danga.com/memcached)
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 scoping per-block.
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. The 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>.
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> (but not from any other instance variables) in a <tt>view_cache</tt> block.
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
- == Declaring dependencies
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
- == View-only caching
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=18926
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
@@ -1,3 +1,6 @@
1
1
 
2
+ * Add test coverage of STI child classes
3
+ * Add anti-dogpiling logic
4
+ * Possibly memoize single-id finders, either per-action or globally with automatic invalidations
2
5
  * Figure out how to cache Builder blocks
3
6
  * Figure out how to cache RJS
data/interlock.gemspec CHANGED
@@ -1,26 +1,26 @@
1
1
 
2
- # Gem::Specification for Interlock-1.0
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.0"
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{2007-12-13}
14
- s.description = %q{An optimal-efficiency caching plugin for Rails.}
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.html.erb", "test/integration/app/app/views/items/recent.html.erb", "test/integration/app/app/views/items/show.html.erb", "test/integration/app/app/views/shared/_related.html.erb", "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/coverage/cache-43041", "test/integration/app/coverage/index.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_controller_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-action_view_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-active_record_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-config_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-core_extensions_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-interlock_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock-memcached_rb.html", "test/integration/app/coverage/vendor-plugins-interlock-lib-interlock_rb.html", "test/integration/app/db/migrate/001_create_items.rb", "test/integration/app/db/schema.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/memcached_test.rb", "TODO", "interlock.gemspec"]
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.9.5}
22
- s.summary = %q{An optimal-efficiency caching plugin for Rails.}
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 = "An optimal-efficiency caching plugin for Rails."
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
- # Build the fragment key from a particular context. This must be deterministic
7
- # and stateful except for the tag. We can't scope the key to arbitrary params
8
- # because the view doesn't have access to which are relevant and which are
9
- # not.
10
- #
11
- # Note that the tag can be pretty much any object. Define #to_interlock_tag
12
- # if you need custom tagging for some class. ActiveRecord::Base already
13
- # has it defined appropriately.
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
- # Mark a controller block for caching. Accepts a list of class dependencies for
45
- # invalidation, as well as a :tag key for explicit fragment scoping.
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
- Interlock.register_dependencies(dependencies, key)
55
-
56
- # See if the fragment exists, and run the block if it doesn't.
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, and stores
88
- # writes in the local process cache too to avoid duplicate memcached requests.
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, content, options = nil)
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
- content
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, and
104
- # checks the local process cache before hitting memcached. Hits on
105
- # memcached are then stored back locally to avoid duplicate requests.
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
- # Interlock.say key, "not found"
200
+ # Not found
201
+ return nil
117
202
  end
118
- content
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
- # Mark a corresponding view block for caching. Accepts a :tag key for
8
- # explicit scoping. You can specify dependencies here if you really want
9
- # to, for example, if you don't have any controller block at all.
10
- #
11
-
12
- def view_cache(*args, &block)
13
- conventional_class = begin; controller.controller_name.classify.constantize; rescue NameError; end
14
- options, dependencies = Interlock.extract_options_and_dependencies(args, conventional_class)
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
- # Interlock.say key "is rendering"
20
-
21
- @controller.cache_erb_fragment(
22
- block,
23
- key,
24
- :ttl => (options.value_for_indifferent_key(:ttl) or Interlock.config[:ttl])
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