trailblazer / cells

View components for Ruby and Rails.
https://trailblazer.to/2.1/docs/cells.html
3.07k stars 236 forks source link

several cache-related questions #401

Open dreyks opened 8 years ago

dreyks commented 8 years ago
  1. It seems that cache does not take into consideration neither template not cell code change. Rails does template digesting to combat this
  2. Say I want only a part of cell template cached: common use-case is "cached html with uncached js". What is the best way to achieve this?

Currently I've come up with something like this

class MyCell < Cell::ViewModel
  cache :html { 'my_cache_key' }

  def show
    call(:html) + render(:js)
  end

  def html
    render :show
  end
end

And then I have show.erb which has all the html (and this state is cached), and (uncached) js.erb that has all the access to cell instance variables

What d'you think?

Btw, for some weird reason I have to write call(:html) + render(:js).html_safe, otherwise the js template gets escaped

apotonick commented 8 years ago

I can only reply to 2.) and I think it looks totally fine to me. The point of Cells is to get away from ugly view fragment caching by modelling your view as an object (with multiple states, if you want that).

Only call with rails-cells is html_safeed, render really only returns the string.

dreyks commented 8 years ago

Ok, got it, thanks

But what about code change? Right now I have to Rails.cache.clear after each deploy that changes code in cell class, which is sub-optimal

apotonick commented 8 years ago

Bring it on, sounds like a good feature!

dreyks commented 8 years ago

This is not as straightforward to implement as I thought. Rails' cache is a view helper, and therefore it knows from which template it was called. Cells' cache on the other hand is called without any knowledge about the template that would render.

Will look into this closer when I have free time

apotonick commented 8 years ago

Yeah, but we have a better internal API where you can find the respective state template and hash it, on the class level. No problem!

I'm here if you need help - maybe join Gitter and we chat there at some point.

dreyks commented 8 years ago

Yeah, that's what I thought too, but then again, someone can do

def show
  render :surprise
end

and how will we know that...

How do I find the chat-room in gitter?

apotonick commented 8 years ago

https://gitter.im/trailblazer/chat

dreyks commented 8 years ago

402

schorsch commented 8 years ago

I would not dive into this template cache digest thing as it over-complicates things.

We rather should go for a simpler cache getter/cleaning methods in the core.

f. ex. this is some code i am using to delete the cache for specific cells:

class SomeCell

  cache :show do
    "#{model.id}/#{model.updated_at.strftime('%Y%m%d%H%M%S')}"
  end

  cache :public_show do
    "#{model.id}/#{model.updated_at.strftime('%Y%m%d%H%M%S')}"
  end

  # Delete the cache for a single or multiple states (views)
  # If states is empty deletes all defined caches
  # Needed whenever a view is updated, for manual deletion of the cache:
  #
  #   Model.select(:id, :updated_at).each{|i| SomeCell.delete_cache( i, :show)}
  #
  # @param [aModel] obj
  # @param [Array<Symbol>, Symbol] states name of cache partial see defs on top
  def self.delete_cache(obj, states)
    state_keys = states.is_a?(Array) ? states : [states]
    state_keys ||= version_procs.keys
    state_keys.each do |i|
      key = cache_key(i, obj)
      #btw. this does not seem to work maybe bcs my local prefix 'cache' in rails.cache_config is not prepended 
      ::Rails.cache.delete(key)   
    end
  end

  # Get a cache key for a given object and state
  # @param [aModel] obj
  # @param [Array<Symbol>, Symbol] states name of cache partial see defs on top
  # @return [String]
  def self.cache_key(state, obj)
    cell = self.(obj)
    # ??
    state_cache_key(state, version_procs[state].evaluate(cell))
  end
end

the specs continue to have a rather ugly lookup:

  describe 'cache keys' do
    it 'has keys' do
      obj = FactoryGirl.create(:a_model)
      cell = cell(:some, obj)
      # WTF ??
      key = cell.class.state_cache_key(:show, cell.class.version_procs[:show].evaluate(cell))
      expect(key).to eq ("cells/some/show/#{obj.id}/#{obj.updated_at.strftime('%Y%m%d%H%M%S')}")
    end
  end

This could be continued with methods to destroy all caches for a cell, all caches for an object across cells, etc. I am using redis + gem readthis for caching and think about to just use http://redis.io/commands/keys command, which by the help of *-lookups would render all of the above useless.

dreyks commented 8 years ago

Yeah, after spending some time fiddling with template digesting I gave up and now use a Capistrano task that clears cell caches for cells whose file were changed between deploys

damien-roche commented 6 years ago

Has anything changed? Recently run into this issue myself. I simply added a 'version' to the cell as part of my cache key, and I change that if I update the code. Not ideal, but it works.

Btw, great gem! I've redesigned a monstrous Rails app recently and persevered with cells for the frontend and it has proven to be much cleaner and simpler to maintain. It is especially useful to bundle assets along with the cells.

PikachuEXE commented 6 years ago

Here is my example for caching: Make cell depends on template + translations + assets

module PokemonTypeCardDefaultStyle

  class Cell < ::SomeBase::Cell

    # === Caching === #
    cache(
      :show,
      :cache_key,
      expires_in: :cache_valid_time_period_length,
      if: :can_be_cached?,
    )

    def can_be_cached?
      # Use following code when child cell(s) is/are used
      # [
      #   child_cell_1,
      # ].all?(&:can_be_cached?)

      true
    end

    def cache_valid_time_period_length
      # Use following code when child cell(s) is/are used
      # [
      #   child_cell_1,
      # ].map(&:cache_valid_time_period_length).min

      # Long time
      100.years
    end

    def self.cache_key
      super.merge(
        # Update this if cell logic updated (code update != logic update)
        logic: :v2017_10_26_1054,
        template: TEMPLATE_FILES_CONTENT_CACHE_KEY,
        translations: TRANSLATION_FILES_CONTENT_CACHE_KEY,
        assets: ASSET_FILES_CONTENT_CACHE_KEY,
      )
    end

    def cache_key
      super.merge(
        current_locale: ::I18n.config.locale,

        pokemon_type: pokemon_type.cache_key,
      )
    end
    # === Caching === #

    def show
      render
    end

    private

    Contract ::Pokemon::Type
    attr_reader :pokemon_type

    TEMPLATE_FILES_CONTENT_CACHE_KEY = begin
      view_folder_path = File.expand_path("views", __dir__)
      file_paths = Dir.glob(File.join(view_folder_path, "**", "*"))

      file_digests = file_paths.map do |file_path|
        next nil unless File.file?(file_path)

        ::Digest::MD5.hexdigest(File.read(file_path))
      end.compact

      ::Digest::MD5.hexdigest(file_digests.join(""))
    end
    private_constant :TEMPLATE_FILES_CONTENT_CACHE_KEY

    TRANSLATION_FILES_CONTENT_CACHE_KEY = begin
      folder_path = ::Rails.root.join(
        "config/locales/path/to/cell/translations",
      )
      file_paths = Dir.glob(File.join(folder_path, "**", "*"))

      file_digests = file_paths.map do |file_path|
        ::Digest::MD5.hexdigest(File.read(file_path))
      end

      ::Digest::MD5.hexdigest(file_digests.join(""))
    end
    private_constant :TRANSLATION_FILES_CONTENT_CACHE_KEY

    ASSET_FILES_CONTENT_CACHE_KEY = begin
      folder_path = ::Rails.root.join(
        *[
          "app/assets/images/path/to/cell/assets",
        ].join("/").split("/"),
      )
      file_paths = Dir.glob(File.join(folder_path, "**", "*"))

      file_digests = file_paths.map do |file_path|
        next nil unless File.file?(file_path)

        ::Digest::MD5.hexdigest(File.read(file_path))
      end

      ::Digest::MD5.hexdigest(file_digests.join(""))
    end
    private_constant :ASSET_FILES_CONTENT_CACHE_KEY

  end
end

If you need to a cell to depend on super cell's cache key:

module PokemonTypeCardCompactStyle

  class Cell < ::PokemonTypeCardDefaultStyle::Cell

    # === Caching === #

    def self.cache_key
      super.merge(
        # Update this if cell logic updated (code update != logic update)
        logic:    [
          super.fetch(:logic, nil),
          :v2018_09_17_1426,
        ].compact.join("+"),
        template: TEMPLATE_FILES_CONTENT_CACHE_KEY,
      )
    end

    def cache_key
      super.merge(
        current_locale: ::I18n.config.locale,

        pokemon_type: pokemon_type.cache_key,
      )
    end
    # === Caching === #

    def show
      render
    end

    TEMPLATE_FILES_CONTENT_CACHE_KEY = begin
      view_folder_path = File.expand_path("views", __dir__)
      file_paths = Dir.glob(File.join(view_folder_path, "**", "*"))

      file_digests = file_paths.map do |file_path|
        next nil unless File.file?(file_path)

        ::Digest::MD5.hexdigest(File.read(file_path))
      end.compact

      ::Digest::MD5.hexdigest(file_digests.join(""))
    end
    private_constant :TEMPLATE_FILES_CONTENT_CACHE_KEY
end
dreyks commented 6 years ago

yeah, i ended up doing something similar two years ago. i'm not using cells right now though