Patrick Brown

@kineticdial

Freeze Your Constants in Ruby

Photo by Agustín Lautaro on Unsplash

Years ago I was working on a very large Ruby on Rails codebase that used constants to hold lists of credit card transaction states. For example:

# example 1
class Txn
  ACTIONABLE_STATES = [:authenticated, :to_settle]
  DONE_STATES = [:settled, :declined]
  # ...
end

However, we had a bug in which a settled transaction could pass a txn.state.in? ACTIONABLE_STATES check. After hours of searching, we found the offending code in a completely different part of the codebase.

# example 2
all_states = ACTIONABLE_STATES.concat(DONE_STATES)

all_states.each do |state|
  # ...
end

If you’re already knowledgeable of the Ruby Array class, you’ve probably spotted the offending line already. #concat mutates the original list, even though it doesn’t end with the idiomatic !, and because ACTIONABLE_STATES is held statically in memory, when this code gets executed it changes ACTIONABLE_STATES state for the rest of that Ruby virtual machine’s life.

In other words, after example 2 gets executed, ACTIONABLE_STATES becomes[:authenticated, :to_settle, :settled, :declined] until you reboot the server.

If you have multiple Ruby boxes running behind a Load Balancer (as we did), this can further obfuscate the problem. Because example 2 might only have been run on certain boxes, you might get cases where one request will not exhibit the bug while another identical one will.

The solution? Freeze your constants.

# example 3
ACTIONABLE_STATES = [:authenticated, :to_settle].freeze
DONE_STATES = [:settled, :declined].freeze

This would cause example 2 to produce a can't modify frozen array (RuntimeError), and if you’ve unit tested example 2, you would’ve hopefully caught this before it was ever even deployed.

More by Patrick Brown

Topics of interest

More Related Stories