skip to Main Content

I have a helper method that does expensive calculations and returns a Hash, and this Hash is constant during my entire application lifespan (meaning it can only change after a re-deploy) and it also doesn’t take any arguments.

For performance, I wish I could ‘cache’ the resulting Hash.

I don’t want to use Rails cache for this, since I want to avoid the extra trip to memcached and I don’t want the overhead of de-serializing the string into a hash.

My first idea was to assign the resulting hash to a Constant and calling .freeze on it. But the helper is an instance method, the constant lives on the class, and I had to do this ultra hacky solution:

module FooHelper

def expensive_calculation_method
  resulting_hash
end

EXPENSIVE_CALCULATION_CONSTANT = Class.new.extend(self).expensive_calculation_method.freeze

This is due to the helper method being an instance method, the helper being a Module (which leads to the fake Class extend so I can call the instance method) and I also must declare the constant AFTER the instance method (if I declare it right after module FooHelper, I get an undefined method 'expensive_calculation_method'.

The second idea was to use memoization, but at least for Rails Controllers memoization is the persistance of a variable over the lifecycle of a single request, so it’s only valuable if you reuse a variable many times from within a single request, which is not my case, but at the same time Helpers are modules, not Classes to be instanciated, and by this point I don’t know what to do.

How would I cache that Hash, or memoize it in a way that persists over requests?

2

Answers


  1. If you want to cache the result of some painful operation at launch time:

    module MyExpensiveOperation
      COMPUTED_RESULT = OtherModule.expensive_operation
    
      def self.cached
        COMPUTED_RESULT
      end
    end
    

    Just make sure that module’s loaded somehow or it won’t initiate. You can always force-require that module if necessary in environment.rb or as a config/initializer type file.

    If you want to lazy load the basic principle is the same:

    module MyExpensiveOperation
      def self.cached
        return @cached if (defined?(@cached))
    
        @cached = OtherModule.expensive_operation
      end
    end
    

    That will handle operations that, for whatever reason, return nil or false. It will run once, and once only, unless you have multiple threads triggering it at the same time. If that’s the case there’s ways of making your module concurrency aware with automatic locks.

    Login or Signup to reply.
  2. Per your comments, this will only change at application boot, so placing it in an initializer would do the trick.

    # config/initializers/expensive_thing.rb
    $EXENSIVE_THING_GLOBAL = expensive_calculation
    
    # or
    EXPENSIVE_THING_CONSTANT = expensive_calculation
    
    # or
    
    Rails.application.config.expensive_thing = expensive_calcualatioin
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search