cache_debugging 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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