cache_debugging 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +29 -0
  5. data/LICENSE +20 -0
  6. data/README.md +104 -0
  7. data/Rakefile +32 -0
  8. data/cache_debugging.gemspec +20 -0
  9. data/lib/cache_debugging.rb +18 -0
  10. data/lib/cache_debugging/cache_blocks.rb +44 -0
  11. data/lib/cache_debugging/digestor.rb +3 -0
  12. data/lib/cache_debugging/railtie.rb +19 -0
  13. data/lib/cache_debugging/strict_dependencies.rb +47 -0
  14. data/lib/cache_debugging/utils.rb +40 -0
  15. data/lib/cache_debugging/version.rb +3 -0
  16. data/lib/cache_debugging/view_sampling.rb +80 -0
  17. data/test/dummy/README.rdoc +28 -0
  18. data/test/dummy/Rakefile +6 -0
  19. data/test/dummy/app/assets/images/.keep +0 -0
  20. data/test/dummy/app/assets/javascripts/application.js +13 -0
  21. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  22. data/test/dummy/app/controllers/application_controller.rb +10 -0
  23. data/test/dummy/app/controllers/concerns/.keep +0 -0
  24. data/test/dummy/app/controllers/workers_controller.rb +7 -0
  25. data/test/dummy/app/helpers/application_helper.rb +2 -0
  26. data/test/dummy/app/mailers/.keep +0 -0
  27. data/test/dummy/app/models/.keep +0 -0
  28. data/test/dummy/app/models/concerns/.keep +0 -0
  29. data/test/dummy/app/models/contract.rb +5 -0
  30. data/test/dummy/app/models/electrician.rb +3 -0
  31. data/test/dummy/app/models/gardener.rb +3 -0
  32. data/test/dummy/app/models/plumber.rb +3 -0
  33. data/test/dummy/app/models/tweet.rb +6 -0
  34. data/test/dummy/app/models/worker.rb +7 -0
  35. data/test/dummy/app/views/electricians/_electrician.html.erb +1 -0
  36. data/test/dummy/app/views/gardeners/_gardener.html.erb +1 -0
  37. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  38. data/test/dummy/app/views/plumbers/_plumber.html.erb +1 -0
  39. data/test/dummy/app/views/workers/_tweets.html.erb +5 -0
  40. data/test/dummy/app/views/workers/_worker.html.erb +12 -0
  41. data/test/dummy/app/views/workers/index.html.erb +17 -0
  42. data/test/dummy/bin/bundle +3 -0
  43. data/test/dummy/bin/rails +4 -0
  44. data/test/dummy/bin/rake +4 -0
  45. data/test/dummy/config.ru +4 -0
  46. data/test/dummy/config/application.rb +27 -0
  47. data/test/dummy/config/boot.rb +5 -0
  48. data/test/dummy/config/database.yml +25 -0
  49. data/test/dummy/config/environment.rb +5 -0
  50. data/test/dummy/config/environments/development.rb +29 -0
  51. data/test/dummy/config/environments/production.rb +80 -0
  52. data/test/dummy/config/environments/test.rb +36 -0
  53. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  54. data/test/dummy/config/initializers/caching.rb +32 -0
  55. data/test/dummy/config/initializers/filter_parameter_logging.rb +4 -0
  56. data/test/dummy/config/initializers/inflections.rb +16 -0
  57. data/test/dummy/config/initializers/mime_types.rb +5 -0
  58. data/test/dummy/config/initializers/secret_token.rb +16 -0
  59. data/test/dummy/config/initializers/session_store.rb +3 -0
  60. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  61. data/test/dummy/config/locales/en.yml +23 -0
  62. data/test/dummy/config/routes.rb +3 -0
  63. data/test/dummy/db/migrate/20130910001826_create_workers.rb +36 -0
  64. data/test/dummy/db/schema.rb +55 -0
  65. data/test/dummy/lib/assets/.keep +0 -0
  66. data/test/dummy/log/.keep +0 -0
  67. data/test/dummy/public/404.html +58 -0
  68. data/test/dummy/public/422.html +58 -0
  69. data/test/dummy/public/500.html +57 -0
  70. data/test/dummy/public/favicon.ico +0 -0
  71. data/test/dummy/test/fixtures/electricians.yml +2 -0
  72. data/test/dummy/test/fixtures/gardeners.yml +2 -0
  73. data/test/dummy/test/fixtures/plumbers.yml +2 -0
  74. data/test/dummy/test/fixtures/workers.yml +17 -0
  75. data/test/integration/cache_debugging_test.rb +81 -0
  76. data/test/test_helper.rb +17 -0
  77. metadata +146 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 966d977f9f2e3f02a1659ef160c7386742b92a16
4
+ data.tar.gz: 069c388fe175aaaae2491748a4e81a387ef7ec5c
5
+ SHA512:
6
+ metadata.gz: ea249f02ce4f52639732bc59d1b53a4017c1cbba3c1c1c4c2e9fd788727d0bf7e1a7bd2635617390b2648ebb9d73e6eeeae255eef10e320ff3a8ef09dc0d8816
7
+ data.tar.gz: 01ef735d0ae31649da1f70aef82ca5de7c8d60d31d8f1df6d05d892da35b6cbace54fb02ebc03087d61a33e9bac99789874cb0f51bc4426728294a40be2ffaa7
@@ -0,0 +1,23 @@
1
+ # See http://help.github.com/ignore-files/ for more about ignoring files.
2
+ #
3
+ # If you find yourself ignoring temporary files generated by your text editor
4
+ # or operating system, you probably want to add a global ignore instead:
5
+ # git config --global core.excludesfile ~/.gitignore_global
6
+
7
+ # Ignore bundler config
8
+ /.bundle
9
+
10
+ # Ignore the default SQLite database.
11
+ *.sqlite3
12
+
13
+ # Ignore all logfiles and tempfiles.
14
+ *.log
15
+ /tmp
16
+
17
+ *.swp
18
+ *.mwb.bak
19
+ *.mwb.beforefix
20
+
21
+ *.DS_Store
22
+
23
+ Gemfile.lock
@@ -0,0 +1,3 @@
1
+ env:
2
+ - "RAILS_VERSION=3.2.0"
3
+ - "RAILS_VERSION=4.0.0"
data/Gemfile ADDED
@@ -0,0 +1,29 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Declare your gem's dependencies in cache_debugging.gemspec.
4
+ # Bundler will treat runtime dependencies like base dependencies, and
5
+ # development dependencies will be added by default to the :development group.
6
+ gemspec
7
+
8
+ rails_version = ENV["RAILS_VERSION"] || "default"
9
+
10
+ rails = case rails_version
11
+ when "default"
12
+ ">= 3.2.0"
13
+ else
14
+ "~> #{rails_version}"
15
+ end
16
+
17
+ gem "rails", rails
18
+
19
+ if rails.match /3\.2\.*/
20
+ gem 'cache_digests', '~> 0.3.0'
21
+ end
22
+
23
+ # Declare any dependencies that are still in development here instead of in
24
+ # your gemspec. These might include edge Rails or gems from your path or
25
+ # Git. Remember to move these dependencies to your gemspec before releasing
26
+ # your gem to rubygems.org.
27
+
28
+ # To use debugger
29
+ # gem 'debugger'
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) {{year}} {{fullname}}
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,104 @@
1
+ # CacheDebugging [![Build Status](https://travis-ci.org/chingor13/cache_debugging.png)](https://travis-ci.org/chingor13/cache_debugging)
2
+
3
+ `cache_debugging` aims to detangle `cache_digests`'s template dependencies. It currently contains 2 different hooks to check for cache integrity.
4
+
5
+ ## StrictDependencies
6
+
7
+ This module ensures that every `render` call within a cache block is called on a partial that is in the callers template dependencies.
8
+
9
+ ### Why?
10
+
11
+ We may not have declared all of our explicit template dependencies.
12
+
13
+ Consider:
14
+
15
+ ```
16
+ cache @workers do
17
+ render @workers
18
+ end
19
+ ```
20
+
21
+ Seems reasonable, as it will assume `workers/worker` is the partial to be depended upon.
22
+
23
+ We could also declaring the template explicitly:
24
+
25
+ ```
26
+ cache @workers do
27
+ render partial: 'workers/worker', collection: @workers
28
+ end
29
+ ```
30
+
31
+ What if, however, `@workers` is a collection of duck-typed objects that behave like workers? We have to explicitly declare the dependencies:
32
+
33
+ ```
34
+ <%# Template Dependency: plumbers/plumber %>
35
+ <%# Template Dependency: gardeners/gardener %>
36
+
37
+ <%# Template Dependency: electricians/electrician %>
38
+ cache @workers do
39
+ render @workers
40
+ end
41
+ ```
42
+
43
+ How do we ensure that all the possible worker types are declared?
44
+
45
+ ### Usage
46
+
47
+ In application.rb (or environment config):
48
+
49
+ ```
50
+ config.cache_debugging.strict_dependencies = true
51
+ ```
52
+
53
+ We keep track of each cache block with it's dependencies and trigger an ActiveSupport notification (`cache_debugging.cache_dependency_missing`) if we're rendering a partial not included any parent's template dependencies. You can handle this notification any way you want.
54
+
55
+ ## View Sampling
56
+
57
+ This module ensures that you have declared all the variable dependencies for your cache block.
58
+
59
+ ### Why?
60
+
61
+ We might be missing a cache variable and not know it.
62
+
63
+ Consider an view of a bug report ticket that can be assigned:
64
+
65
+ ```
66
+ # tickets/index.html.erb
67
+ <% cache @tickets do %>
68
+ <%= render @tickets %>
69
+ <% end %>
70
+
71
+ # tickets/_ticket.html.erb
72
+ <% cache ticket do %>
73
+ <tr>
74
+ <td><%= ticket.id %></td>
75
+ <td><%= ticket.title %></td>
76
+ <td><%= ticket.assigned_to.name %></td>
77
+ </tr>
78
+ <% end %>
79
+ ```
80
+
81
+ Let's say we decided to replace `assigned_to.name` with "me" if the ticket is assigned to me. So we update the ticket partial:
82
+
83
+ ```
84
+ # tickets/_ticket.html.erb
85
+ <% cache(ticket, current_user) do %>
86
+ <tr>
87
+ <td><%= ticket.id %></td>
88
+ <td><%= ticket.title %></td>
89
+ <td><%= ticket.assigned_to == current_user ? "me" : ticket.assigned_to.name %></td>
90
+ </tr>
91
+ <% end %>
92
+ ```
93
+
94
+ We've updated the cache block for the singular ticket, but forgotten to update the collection cache block. We won't be notified that we're rendering a different view than we expect.
95
+
96
+ ### Usage
97
+
98
+ In application.rb (or environment config):
99
+
100
+ ```
101
+ config.cache_debugging.view_sampling = 0.1
102
+ ```
103
+
104
+ Every X% of cache hits (10% for the above example), we will re-render the cache block anyways and compare the results. If they don't match, we trigger and ActiveSupport notification (`cache_debugging.cache_mismatch`). You can handle this notification any way you want.
@@ -0,0 +1,32 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'CacheDebugging'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+ Bundler::GemHelper.install_tasks
21
+
22
+ require 'rake/testtask'
23
+
24
+ Rake::TestTask.new(:test) do |t|
25
+ t.libs << 'lib'
26
+ t.libs << 'test'
27
+ t.pattern = 'test/**/*_test.rb'
28
+ t.verbose = false
29
+ end
30
+
31
+
32
+ task default: :test
@@ -0,0 +1,20 @@
1
+ # Provide a simple gemspec so you can easily use your enginex
2
+ # project in your rails apps through git.
3
+ require File.expand_path('../lib/cache_debugging/version', __FILE__)
4
+ Gem::Specification.new do |s|
5
+ s.name = "cache_debugging"
6
+ s.version = CacheDebugging::VERSION
7
+ s.description = 'Verify cache key dependencies'
8
+ s.summary = 'Verify cache key dependencies via random sampling'
9
+ s.add_dependency "rails", ">= 3.2.0"
10
+
11
+ s.author = "Jeff Ching"
12
+ s.email = "ching.jeff@gmail.com"
13
+ s.homepage = "http://github.com/chingor13/cache_debugging"
14
+ s.license = "MIT"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = Dir.glob('test/*_test.rb')
18
+
19
+ s.add_development_dependency "sqlite3"
20
+ end
@@ -0,0 +1,18 @@
1
+ require 'rails'
2
+ if Rails::VERSION::MAJOR != 4
3
+ begin
4
+ require 'cache_digests'
5
+ rescue LoadError
6
+ raise "You must use the 'cache_digests' gem if you are running Rails 3"
7
+ end
8
+ end
9
+
10
+ module CacheDebugging
11
+ autoload :CacheBlocks, 'cache_debugging/cache_blocks'
12
+ autoload :StrictDependencies, 'cache_debugging/strict_dependencies'
13
+ autoload :ViewSampling, 'cache_debugging/view_sampling'
14
+ autoload :Digestor, 'cache_debugging/digestor'
15
+ autoload :Utils, 'cache_debugging/utils'
16
+ end
17
+
18
+ require 'cache_debugging/railtie'
@@ -0,0 +1,44 @@
1
+ module CacheDebugging
2
+ module CacheBlocks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ alias_method_chain :cache, :blocks
7
+ end
8
+
9
+ # every time we start a cache block, we want to store the template and the block's dependencies
10
+ def cache_with_blocks(name = {}, options = nil, &block)
11
+ if current_template
12
+ dependencies = Digestor.new(
13
+ current_template,
14
+ lookup_context.rendered_format || :html, ApplicationController.new.lookup_context
15
+ ).nested_dependencies
16
+
17
+ cache_blocks.push({
18
+ template: current_template,
19
+ dependencies: Utils.deep_flatten(dependencies)
20
+ })
21
+ ret = cache_without_blocks(name, options, &block)
22
+ cache_blocks.pop
23
+ ret
24
+ else
25
+ cache_without_blocks(name, options, &block)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def cache_blocks
32
+ @cache_blocks ||= []
33
+ end
34
+
35
+ def current_template
36
+ @virtual_path
37
+ end
38
+
39
+ def cache_depth
40
+ cache_blocks.length
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,3 @@
1
+ module CacheDebugging
2
+ Digestor = CacheDigests::TemplateDigestor rescue ActionView::Digestor
3
+ end
@@ -0,0 +1,19 @@
1
+ module CacheDebugging
2
+ class Railtie < Rails::Railtie
3
+ config.cache_debugging = ActiveSupport::OrderedOptions.new(
4
+ # raise exceptions if templates aren't in the dependency tree
5
+ strict_dependencies: false,
6
+
7
+ # [0,1] - decimal (percent) of cache hits to check
8
+ view_sampling: 0
9
+ )
10
+
11
+ initializer "cache_debugging.setup", :before => 'cache_digests' do |app|
12
+ ActiveSupport.on_load(:action_view) do
13
+ include CacheDebugging::CacheBlocks
14
+ include CacheDebugging::StrictDependencies
15
+ include CacheDebugging::ViewSampling
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,47 @@
1
+ module CacheDebugging
2
+ module StrictDependencies
3
+ extend ActiveSupport::Concern
4
+
5
+ def self.strict_dependencies_enabled?
6
+ !!Rails.application.config.cache_debugging.strict_dependencies
7
+ end
8
+
9
+ included do
10
+ alias_method_chain :render, :template_dependencies
11
+ end
12
+
13
+ # every time we render, we want to check if the partial is in the dependency list
14
+ def render_with_template_dependencies(*args, &block)
15
+ if CacheDebugging::StrictDependencies.strict_dependencies_enabled? && cache_blocks.length > 0
16
+ options = args.first
17
+ if options.is_a?(Hash)
18
+ if partial = options[:partial]
19
+ validate_partial!(partial)
20
+ end
21
+ else
22
+ (options.respond_to?(:to_ary) ? options.to_ary : Array(options)).each do |object|
23
+ validate_partial!(Utils.object_partial_path(object))
24
+ end
25
+ end
26
+ end
27
+ render_without_template_dependencies(*args, &block)
28
+ end
29
+
30
+ private
31
+
32
+ def validate_partial!(partial)
33
+ unless valid_partial?(partial)
34
+ Utils.publish_notification("cache_debugging.cache_dependency_missing", {
35
+ partial: partial,
36
+ template: cache_blocks.last[:template],
37
+ dependencies: cache_blocks.last[:dependencies]
38
+ })
39
+ end
40
+ end
41
+
42
+ def valid_partial?(partial)
43
+ cache_blocks.last[:dependencies].include?(partial)
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,40 @@
1
+ # Use this module to keep helper methods out of ActionView::Base scope
2
+ module CacheDebugging
3
+ module Utils
4
+ # recursively flatten complex array/hash objects
5
+ def self.deep_flatten(array_or_hash)
6
+ case array_or_hash
7
+ when Array
8
+ array_or_hash.map do |value|
9
+ if value.is_a?(Hash) || value.is_a?(Array)
10
+ deep_flatten(value)
11
+ else
12
+ value
13
+ end
14
+ end.flatten
15
+ when Hash
16
+ deep_flatten(array_or_hash.keys + array_or_hash.values)
17
+ end
18
+ end
19
+
20
+ # wrapper for ActiveSupport::Notification publish
21
+ def self.publish_notification(name, extra = {})
22
+ ActiveSupport::Notifications.publish(
23
+ name,
24
+ Time.now, # not a block, so fake the start time
25
+ Time.now, # not a block, so fake the finish time
26
+ SecureRandom.hex(10), # generate a unique id
27
+ extra
28
+ )
29
+ end
30
+
31
+ def self.object_partial_path(object)
32
+ partial = begin
33
+ object.to_partial_path
34
+ rescue
35
+ object.class.model_name.partial_path
36
+ end
37
+ partial.split("/").tap{|parts| parts.last.gsub!(/^_?/, "_")}.join("/")
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,3 @@
1
+ module CacheDebugging
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,80 @@
1
+ module CacheDebugging
2
+ module ViewSampling
3
+ extend ActiveSupport::Concern
4
+
5
+ mattr_accessor :force_sampling
6
+
7
+ def self.force_sampling?
8
+ !!self.force_sampling
9
+ end
10
+
11
+ def self.view_sampling_rate
12
+ Rails.application.config.cache_debugging.view_sampling
13
+ end
14
+
15
+ def self.view_sampling_enabled?
16
+ !!view_sampling_rate
17
+ end
18
+
19
+ # code taken from fragment_for
20
+ def self.render_block(view, &block)
21
+ # VIEW TODO: Make #capture usable outside of ERB
22
+ # This dance is needed because Builder can't use capture
23
+ output_buffer = view.output_buffer
24
+ pos = output_buffer.length
25
+ yield
26
+ output_safe = output_buffer.html_safe?
27
+ fragment = output_buffer.slice!(pos..-1)
28
+ if output_safe
29
+ view.output_buffer = output_buffer.class.new(output_buffer)
30
+ end
31
+ fragment
32
+ end
33
+
34
+ def self.should_sample?(options)
35
+ return false unless view_sampling_enabled?
36
+ return true if force_sampling?
37
+
38
+ sample = (options || {}).fetch(:sample) { view_sampling_rate }.to_f
39
+ rand <= sample
40
+ end
41
+
42
+ included do
43
+ alias_method_chain :cache, :view_sampling
44
+ end
45
+
46
+ # clear forcing of view sampling if it's been initiated
47
+ def cache_with_view_sampling(name = {}, options = nil, &block)
48
+ cache_without_view_sampling(name, options, &block)
49
+ CacheDebugging::ViewSampling.force_sampling = false if cache_depth == 0
50
+ end
51
+
52
+ private
53
+
54
+ # since there are no hooks on a cache hit, that also has access to the render block, we
55
+ # must override fragement_for here
56
+ def fragment_for(name = {}, options = nil, &block)
57
+ if fragment = controller.read_fragment(name, options)
58
+ return fragment unless CacheDebugging::ViewSampling.should_sample?(options)
59
+ CacheDebugging::ViewSampling.force_sampling = true
60
+
61
+ CacheDebugging::ViewSampling.render_block(self, &block).tap do |uncached|
62
+ Utils.publish_notification("cache_debugging.cache_mismatch", {
63
+ cached: fragment,
64
+ uncached: uncached,
65
+ template: current_template,
66
+ cache_key: name
67
+ }) unless uncached == fragment
68
+ end
69
+ else
70
+ fragment = CacheDebugging::ViewSampling.render_block(self, &block)
71
+ controller.write_fragment(name, fragment, options)
72
+ end
73
+ end
74
+
75
+ def current_template
76
+ @virtual_path
77
+ end
78
+
79
+ end
80
+ end