I successfully upgraded a Rails application from version 2.3 to 3.2. Or so I thought.
"The Best Laid Plans..."
Though I read lots of blog posts; though I watched the excellent
videos from RailsCasts; though I used the
rails_upgrade plugin; though
I made and worked through my own checklist; though all the tests passed;
though manual testing found no issues; it wasn't long after deploying
to production that I received the first email from our exception
In brief, there was a problem sending email caused by an inability to
DelayedMessage#message for old records.
Here's the simplified code for
DelayedMessage and its use. Can you
spot the problem?
Email is defined by the "Internet Message Format" specified in
RFC 2282. But, the code above
is not storing the email that way. It's storing a
type returned from the
ActionMailer APIs in Rails 2. Why is this a
problem? Rails 3 changed its implementation of email. It moved from the
tmail gem to the
ActionMailer APIs now return a
Mail::Message instead of a
How, then, can we change the system to (1) work with existing
DelayedMessage records and (2) be more resilient to any future
changes in the email implementation in Rails?
Step 1: Store Data Instead of a Type
Instead of storing an instance of the message, a
store the string representation of the message.
We have two things working in our favor here. First, since
DelayedMessage#message is a
text column in our database, we don't
have to change it to be able to store the message as a long string.
Mail::Message#to_s returns the message as a string that
conforms to RFC 2282.
Step 2: Read Old and New Messages
Now that we store the message as a string, let's retrieve it as one,
too. We take control of retrieval by providing our own implementation
By always converting the stored message to a string during retrieval,
we can determine whether the message is in the old format, a
TMail::Mail. Either way, we can call
#to_s on it to
ensure we're working with a string representation that conforms to
RFC 2282, since we either
stored it that way (our earlier change) or obtain that result from
TMail::Mail#to_s. Then, all we have to do is use
to parse the string into a
Mail::Message instance, which we can then
use with Rails 3.
The Only Constant in Life is Change
One of the many changes introduced in Rails 3 was the API for sending email. Though the API changed, the observable behavior, that an email was sent, remained the same. Rails 2 and 3 use an email implementation that conforms to the "Internet Message Format" in RFC 2282. Yet, the application managed to subvert that conformance by storing a type. While we can overcome such problems, it's best to avoid them by storing data instead.