For over a month, I received occasional reports that an existing feature, adding secondary accounts, wasn't working. I could never reproduce the failure in any browser or with a unit test, though.
Recently, I received a report about another feature, creating an account, that failed. I could see it fail if I watched the application log while the feature was used, but, again, I couldn't reproduce the failure.
Calling for Reinforcements
I revisited the problem this week. After many more attempts at reproducing the problem, I was out of ideas. I called for reinforcements. I needed another pair of eyes on this one.
It wasn't long before there was a spark! My pair remembered facing a problem on an old project. It involved mass-assignment. The feature on my project used mass-assignment. It involved overwritten mutators. The feature on my project used overwritten mutators. Could this problem be the same?
The Overwritten Mutators
Here are the overwritten mutators, password=
and password_confirmation=
.
They either store their value locally or on an object backed by LDAP. This decision is made in store_locally?
, which checks another attribute named role_type
.
The Problem
Here's the code that has been intermittently failing.
It would cause the password confirmation validation to fail, complaining that the provided password and password confirmation didn't match.
The Solution
password=
and password_confirmation=
, through store_locally?
, depend on role_type
. Though create_login_account
builds the hash of attributes in an order which puts role_type
first, there is no guarantee 1 on the order in which the attributes will be used to assign the attributes on Account
. Thus, password
or password_confirmation
could be assigned before role_type
.
To ensure that role_type
is assigned before password
and password_confirmation
, we abandon mass-assignment, instead assigning the attributes one at a time.
This approach, though not necessarily idiomatic, solves the problem. I added a brief comment in the project so that anyone, including me, who works on this code later will know why it deviates from the norm.
Notes
- There's no guarantee in Ruby 1.8, anyway, which this application uses. In Ruby 1.8, “The order in which you traverse a hash by either key or value may seem arbitrary, and will generally not be in the insertion order.” In Ruby 1.9 and 2.0, “Hashes enumerate their values in the order that the corresponding keys were inserted.”