Stan Lo
Stan Lo
Jun 22, 2018 · 4 min read

This is the first post of the series. What I want to do is to give you a general idea about how Rails' components interact with each other when you use its functionalities (like fragment caching). However, this is the first time I write this kind of article, so please leave a comment if you have any advice about it.

I believe most of you know or have used Rails’ fragment caching. But you probably don’t know how it works. So today I’m going to give you a short introduction about how does fragment caching work underneath (on Rails 5.2).

Let’s first see what components (classes/modules) are involved when performing fragment caching:

And of course I didn’t list all modules/classes here, just to give you a general idea.

User Interface

Fragment caching’s usage is very simple, just call the method like:

<% cache project do %>
<b>All the topics on this project</b>
<%= render project.topics %>
<% end %>

And this method is from a view helper called

# actionview/lib/action_view/helpers/cache_helper.rbmodule ActionView
module Helpers
module CacheHelper
def cache(name = {}, options = {}, &block)
if controller.respond_to?(:perform_caching) && controller.perform_caching
name_options = options.slice(:skip_digest, :virtual_path)
safe_concat(fragment_for(cache_fragment_name(name, name_options), options, &block))
else
yield
end

nil
end
endendend

It mainly does two things

  • generate a caching key with
  • use to cache the block

Let’s see what does

# actionview/lib/action_view/helpers/cache_helper.rbdef fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
......
content
else
......
write_fragment_for(name, options, &block)
end
end

It’s job is very simple: read cache content if it there’s any, otherwise write the content. And besides read/write cache content, it also reports the cache hit result to template renderer:

# actionview/lib/action_view/helpers/cache_helper.rbdef fragment_for(name = {}, options = nil, &block)
if content = read_fragment_for(name, options)
@view_renderer.cache_hits[@virtual_path] = :hit if defined?(@view_renderer)
......
else
@view_renderer.cache_hits[@virtual_path] = :miss if defined?(@view_renderer)
......
end
end

This is how Rails know if the cache is hit or not. And in order to read/write the cache content, uses and to do the job:

# actionview/lib/action_view/helpers/cache_helper.rbdef read_fragment_for(name, options)
controller.read_fragment(name, options)
end

def write_fragment_for(name, options)
pos = output_buffer.length
yield
......
fragment = output_buffer.slice!(pos..-1)
......
controller.write_fragment(name, fragment, options)
end

As you can see they eventually calls and on a variable named . And your guess is right, the controller is our Rails controller that serves current request. So now the subject becomes the controller instead of view. If you trace the source of then you'll find it's located in .

Main Logic — AbstractController::Caching::Fragments

This module provides three essential actions for dealing with cache: , and

  # actionpack/lib/abstract_controller/caching/fragments.rb  # instrumentation code are removed to reduce the content
def write_fragment(key, content, options = nil)

......

content = content.to_str
cache_store.write(key, content, options) # <- write cache content to cache store
content
end

def read_fragment(key, options = nil)
...... result = cache_store.read(key, options) # <- read cache content from cache store
result.respond_to?(:html_safe) ? result.html_safe : result
end

def expire_fragment(key, options = nil)
...... if key.is_a?(Regexp)
cache_store.delete_matched(key, options) # <- delete multiple cache contents from cache store
else
cache_store.delete(key, options) # <- delete cache content from cache store
end
end

We can think this module as an abstraction layer between user interface (the method) and the cache storage. It'll be included by so you can call all these methods inside your controllers.

Fundamental Part — ActiveSupport::Cache::Store

Next component I’m going to talk about is . It defines the universal interface for different cache storage like , , ..etc. You can see them by running:

# Rails 5.2.0
ActiveSupport::Cache::Store.descendants
#=> [ActiveSupport::Cache::Strategy::LocalCache::LocalStore,
# ActiveSupport::Cache::FileStore,
# ActiveSupport::Cache::MemoryStore,
# ActiveSupport::Cache::RedisStore]

Note that it won’t list some classes unless you installed corresponding gem because Rails won’t be able to load the them (i.e. you need to install to load )

Some of the commonly used interfaces are , , and . But I am not going to show each of them's implementation cause it'll be too messy. Instead I'm going to use method as example:

  # activesupport/lib/active_support/cache.rb  # instrumentation code are removed to reduce the content
def read(name, options = nil)
options = merged_options(options)
key = normalize_key(name, options)
version = normalize_version(name, options)

entry = read_entry(key, options)

if entry
if entry.expired?
delete_entry(key, options)
nil
elsif entry.mismatched?(version)
nil
else
entry.value
end
else
nil
end
end
end

The , (and ) methods here are the interface that each cache storage class needs to implement

# activesupport/lib/active_support/cache.rbdef read_entry(key, options)
raise NotImplementedError.new
end

def write_entry(key, entry, options)
raise NotImplementedError.new
end

Summary

I hope above walk through gives a basic understanding about how the fragment caching works underneath. Please leave a comment if you have any advice or feedback. And if you want to see more post like this one, please let me know as well, so I won’t have any excuse to be lazy.

Ruby Inside

Ruby articles and posts

Stan Lo

Written by

Stan Lo

Creator of Goby language(https://github.com/goby-lang/goby), also a Rails/Ruby developer. Love open source, cats and boxing.

Ruby Inside

Ruby articles and posts