Recently someone at work asked about class instance variables. We had recently been told about a catastrophic incident wherein data was being shared with the wrong objects because a class method set an instance variable, then later used that instance variable to do some important work.

If you’re familiar with variable ownership in Ruby, you can probably see where this is going. Since each class is an instance of Class, an instance variable in a class method belongs to the class. Each time that method is called, the exact same instance variable is referenced. In a multi-threaded context, this can have some very undesirable side effects.

I’ll create a contrived scenario to illustrate. Say we’re a bank and we’ve got customers who want their balance emailed to them each night. We assume customers that carry no balance shouldn’t get emails. We’ve created BalanceNotice to handle this.

class BalanceNotice
  def self.mail(email, balance)
    @balance = balance.to_f

    if should_email?
      BalanceMailer.mail(to: email, subject: "Your balance is #{@balance}")
    end
  end

  def should_email?
    !@balance.zero?
  end
end

Things are looking great: tests pass, the product team loves it, it looks great in the staging environment. Time to ship! Then the first night it’s out in its new multi-threaded production environment calls start coming in from frantic customers. “Someone must have stolen my identity.” “Freeze my account, now!” Something is wrong. They’re receiving emails with balances from other accounts.

Here’s how that could happen:

 BalanceNotice.mail("joe@joe.com", 50) |
     @balance = 50.0                   |
     should_email? => true             |
                                       | BalanceNotice.mail("ann@ann.com", 9000)
                                       |     @balance = 9000.0
     BalanceMailer.mail(               |
       "joe@joe.com",                  |
       "Your balance is $9000.0"       |
     )                                 |
                                       |     should_email? => true
                                       |     BalanceMailer.mail(
                                       |       "ann@ann.com",
                                       |       "Your balance is $9000.0"
                                       |     )

While Ann is happy with this new feature, Joe is ecstatic (for now). BalanceNotice.mail was called with his balance as an argument, but before his email was sent @balance was re-bound to Ann’s balance.

Unless you anticipate this behavior, this bug might be hard to find. However, it is pretty simple to fix. By replacing the class instance variable (@balance) with a local variable (balance), and passing that local variable to should_email? as an argument, sanity is returned.

class BalanceNotice
  def self.mail(email, balance)
    balance = balance.to_f

    if should_email?(balance)
      BalanceMailer.mail(to: email, subject: "Your balance is #{balance}")
    end
  end

  def should_email?(balance)
    !balance.zero?
  end
end

Now each time .mail is called balance will be local to that call. Joe will get Joe’s balance (with great disappointment) and Ann will continue to get Ann’s balance.