Skip to content

Instantly share code, notes, and snippets.

@blimmer
Created September 27, 2017 15:04
Show Gist options
  • Save blimmer/20b4e376bce851df1c06af0f529a071a to your computer and use it in GitHub Desktop.
Save blimmer/20b4e376bce851df1c06af0f529a071a to your computer and use it in GitHub Desktop.

Revisions

  1. Ben Limmer created this gist Sep 27, 2017.
    80 changes: 80 additions & 0 deletions foo_for_caching.rb
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,80 @@
    # This class exists to make the caching of Foo objects more efficient.
    # Marshaling full activerecord objects is expensive, especially in Rails 5
    # because of this bug: https://github.com/rails/rails/issues/30680
    # This simple ruby class selects a very small subset of parameters needed
    # for common operations on these objects. It also acts like Foo, by
    # passing attributes through to an in-memory Foo model.
    class FooForCaching
    attr_accessor :attributes

    # This is a list of attributes we want to save in cache
    ATTRIBUTES_CACHED = %i(
    id
    bar
    baz
    ).freeze

    # These are methods that we've ensured we have the cached attributes to properly
    # calculate.
    SAFE_PASSTHROUGH_METHODS = %i(
    my_method_1
    my_method_2
    ).concat(ATTRIBUTES_CACHED).freeze

    # Constructor
    # @param [Hash] attributes the attributes to cache. attributes not provided will be interpreted to be `nil`
    def initialize(attributes)
    @attributes = attributes.symbolize_keys.slice(*ATTRIBUTES_CACHED)
    end

    # Don't create the in-memory activerecord model to access the simple id
    # attribute.
    def id
    attributes[:id]
    end

    # Pass undefined attribute methods to a memoized, in-memory Foo object
    # to allow running methods on an ActiveRecord Foo object.
    def method_missing(method, *args)
    unless SAFE_PASSTHROUGH_METHODS.include?(method)
    log_unsafelisted_method_passthrough(method)
    end

    if respond_to_missing?(method)
    ar_model_with_attrs.public_send(method, *args)
    else
    super
    end
    end

    # See https://rubocop.readthedocs.io/en/latest/cops_style/#stylemethodmissing
    def respond_to_missing?(method, *args)
    ar_model_with_attrs.public_methods.include?(method) || super(method, args)
    end

    # Do not accidentally cache the in-memory ActiveRecord object if it was created
    # before the Marshal dump was called. Caching that object would re-introduce
    # the problem that this class intends to solve.
    def marshal_dump
    @attributes
    end

    # Set the attributes instance variable when this object is unmarshalled from
    # cache.
    def marshal_load(attributes)
    @attributes = attributes
    end

    private

    def ar_model_with_attrs
    @ar_model = Foo.new(@attributes).tap(&:readonly!)
    end

    # exposed for testing
    def log_unsafelisted_method_passthrough(method)
    Rails.logger.warn(
    "[FooForCaching] Method #{method} called. This method will be delegated, but it's not explicitly marked as safe."
    )
    end
    end