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]

  # ...

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

# example 2

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, 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.

Discuss on