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
check. After hours of searching, we found the offending code in a completely different part of the
# 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,
[: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.
Discuss on Lobste.rs