Freeze Your Constants in Ruby
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
ACTIONABLE_STATES = [:authenticated, :to_settle]
DONE_STATES = [:settled, :declined]
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|
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,
[: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.