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 notifier.
In brief, there was a problem sending email caused by an inability to deserialize
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
TMail::Mail, the 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
TMail::Mail, we 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. Second,
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 of
By always converting the stored message to a string during retrieval, we can determine whether the message is in the old format, a serialized
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
Mail.read_from_string 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.