paint-brush
Freeze Your Constants in Rubyby@kineticdial
861 reads
861 reads

Freeze Your Constants in Ruby

by Patrick BrownJanuary 12th, 2019
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

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:

Company Mentioned

Mention Thumbnail
featured image - Freeze Your Constants in Ruby
Patrick Brown HackerNoon profile picture

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.