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?
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 mail
gem. Consequently, the ActionMailer
APIs now return a Mail::Message
instead of a TMail::Mail
.
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 DelayedMessage#message
.
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.