Module: Card::View::Cache

Includes:
CacheAction, Stub
Included in:
Card::View
Defined in:
lib/card/view/cache.rb,
lib/card/view/cache/stub.rb,
lib/card/view/cache/cache_action.rb

Overview

View::Cache supports smart card view caching.

The basic idea is that when view caching is turned on (via config.view_cache), we try to cache a view whenever it's "safe" to do so. We will include everything inside that view (including other views) until we find something that isn't safe. When something isn't safe, we render a stub: a placeholder with all the info we need to come back and replace it with the correct content later. In this way it is possible to have many levels of cached views within cached views.

Here are some things that we never consider safe to cache:

  1. a view explicitly configured never to be cached
  2. a view of a card with view-relevant permission restrictions
  3. a view other than the requested view (eg a denial view)
  4. a card with unsaved content changes

We also consider it unsafe to cache a view of one card within a view of a different card, so nests are always handled with a stub.

Cache configuration

Cache settings (#5) can be configured in the view definition and (less commonly) as a view option.

By far, the most common explicit caching configuration is :never. This setting is used to prevent over-caching, which becomes problematic when data changes do not clear the cache.

Generally speaking, a card is smart about clearing its own view caches when anything about the card itself. So when I update the card "Johnny", all the cached views of "Johnny" are cleared. Similarly, changes to structure rules and other basic patterns are typically well managed by the caching system.

However, there are many other potential changes that views cannot detect. Views that are susceptible to these "cache hazards" should be configured with cache: :never.

Cache hazards

If a view contains any of the following cache hazards, it would be wise to consider a cache: :never configuration:

  • dynamic searches (eg Card.search) whose results may change
  • live timestamps (eg Time.now)
  • environmental variables (eg Env.params)
  • any variables altered in one view and used in another (eg @myvar)
  • other cards' properties (eg Card["random"].content)

What all of the above have in common is that they involve changes about which the view caching system is unaware. This means that whether the cache hazard is rendered directly in a view or just used in its logic, it can change in a way that should change the view but won't change the view if it's cached.

Altering cached views

Whereas ignoring cache hazards may cause over-caching, altering cached views may cause outright errors. If a view directly alters a rendered view, it may be dangerous to cache.

   # obviously safe to cache
   view(:x) { "ABC" }

   # also safe, because x is NOT altered
   view(:y) { render_x + "DEF" }

   # unsafe and thus never cached, because x is altered
   view(:z, cache: :never) { render_x.reverse }

Specifically, the danger is that the inner view will be rendered as a stub, and the out view will end up altering the stub and not the view.

Although alterations should be considered dangerous, they are actually only problematic in situations where the inner view might sometimes render a stub. If the outer view is rendering a view of the same card with all the same view settings (perms, unknown, etc), there will be no stub and thus no error. Remember, however, that a view on a narrow set may inherit view settings from a general set. To be confident that a view alteration is safe, all inherited settings must be taken into account.

Caching Best Practices

Here are some good rules of thumb to make good use of view caching:

  1. Use nests. If you can show the content of a different card with a nest rather than by showing the content directly, the caching system will be much happier with you.

    view :bad_idea, cache: :never do Card["random"].content end

    view :good_idea do nest :random, view: :core end

  2. Isolate the cache hazards. Consider the following variants:

    view :bad_idea, cache: :never do if morning_for_user? expensive_good_morning else expensive_good_afternoon end end

    view :good_idea, cache: :never do morning_for_user? ? render_good_morning : render_good_afternoon end

    In the first example, we have to generate expensive greetings every time we render the view. In the second, only the test is not cached.

  3. If you must alter view results, consider generating the view content in a separate method.

    # First Attempt

    view :hash_it_in do { cool: false } end

    view :bad_idea, cache: :never do render_badhash.merge sucks: true end

    #Second Attempt

    view :hash_it_out do hash_it_out end

    def hash_it_out { cool: true } end

    view :good_idea do hash_it_out.merge rocks: true end

    The first attempt will work fine with caching off but is risky with caching on. The second is safe with caching on.

Optimizing with :always

It is never strictly necessary to use cache: :always, but this setting can help optimize your use of the caching system in some cases.

Consider the following views:

   view(:hat) { "hat" } # ...but imagine this is computationally expensive

   view(:old_hat)  { "old #{render_hat}"  }
   view(:new_hat)  { "new #{render_hat}"  }
   view(:red_hat)  { "red #{render_hat}"  }
   view(:blue_hat) { "blue #{render_hat}" }

Whether "hat" uses :standard or :always, the hat varieties (old, new, etc...) will fully contain the rendered hat view in their cache. However, with :standard, the other views will each re-render hat without attempting to cache it separately or to find it in the cache. This could lead to man expensive renderings of the "hat" view. By contrast, if the cache setting is :always, then hat will be cached and retrieved even when it's rendered inside another cached view.

Defined Under Namespace

Modules: CacheAction, ClassMethods, Stub