Instance variable module memoization does not work

This is part of the Semicolon&Sons Code Diary - consisting of lessons learned on the job. You're in the caching category.

Last Updated: 2021-05-16

I was getting some bugs in my tests with code like the following. Specifically, the conversion wasn't tracking.

(All code gets executed in the same request, FYI)

module ApplicationHelper
  def order_just_completed?(order)
    @order_just_completed ||=
      cookies.signed.delete(:order_just_completed).present? && order.present?
  end
end

class AccountController
  include ApplicationHelper


  def show_downloads
    # This call or order_just_completed? goes first and works
    unless helpers.order_just_completed?(@order)
      @possible_upgrades = @order.possible_upgrades
      @possible_bundle_upgrades = @order.possible_bundle_upgrades
    end
    render "my_view"
  end

end

View code:

# This call to order_just_completed? goes second and does not work...
<% if order_just_completed?(@order) %>
  <%= tag.div(data: {
    conversion: 'purchase', currency: @order.currency, value: @order.total.to_s, transaction_id: @order.number
  }) %>

If you look closely at the order_just_completed? function, it modifies the session data after being called once. However it does cache the result in an instance variable @order_just_completed. The issue, then, is that this instance variable, being in module, which itself has no instances, is only available to whatever particular other class instance (e.g. controller) happened to first call order_just_completed? The code in the view file does not share this module state.

To make this clearer notice how the caching is not shared across instances.

module SharedCode
  def fav_number
    @number ||= rand
  end
end

class Dog
  include SharedCode
end

class Cat
  include SharedCode
end

Dog.new.fav_number
# e.g. 0.5010301301
Cat.new.fav_number
# e.g. 0.78122112

Essentially the same thing was happening in my Rails app.

The quick-fix solution was to use a global variable instead

module SharedCode
  def fav_number
    @_shared_code_cache_number ||= rand
  end
end

Lesson

Instance variable memoization inside a module/mixin will not carry over to other instances. Rethink your design or consider reaching for a global variable.