Upgrading to Rails 3.1 on Heroku — Part III: Rails

Upgrading to Rails 3.1 on Heroku — Part III: Rails

Up to this point in this series I have been focused on some of the prerequisites to upgrading Rails to 3.1. In this post, all of those requirements have been satisfied, and I can finally get to the meat of the task: upgrading the Rails gem!

First, to Rails 3.0.10

Just to be sure everything is fine and there’s no major deprecation warnings, I’ll upgrade to and deploy Rails 3.0.10 before I jump to Rails 3.1.

And now for Rails 3.1

Ryan Bates has produced a pretty good Railscast on upgrading to Rails 3.1, so I will refer you to his site for the nitty-gritty details and use this post to instead focus on my experience. Full details on the changes in Rails 3.1 can be found in the release notes, if you are interested.

Updating the Rails gem

The first step in upgrading is to update the Gemfile and run bundle update rails. At the time of this post, Rails 3.1.1 was out, so I’ll use that.

gem 'rails', '3.1.1'

I further went ahead and did bundle update on the rest of my gems since it had been a good long while and many of them probably had newer, more Rails-3.1-friendly versions out.

Updating configuration

There’s very little configuration change that has to happen with Rails 3.1, and I will again defer to the railscast for general details.

If you want to go gung-ho and fully upgrade your configurations, you can use the rake rails:upgrade task, which will attempt to replace your configuration files or let you know that they are up to date. My advice would be to ensure you are fully committed to SCM, let the rake task overwrite your files, and then use a diff tool like GitX or Changes to compare the files before and after. This way, you’ll be starting with clean, fresh configurations and you can merge back in any customization you made along the way before committing the new configuration files.

Resolving issues

Once all the changes and configuration were finished, I just needed to do some QA to make sure everything worked. Unfortunately, it didn’t quite go as smoothly as I’d hoped.

Session store issues

One issue I ran into was with the session store. My browser was “logged in” and had a cookie containing a session from Rails 3.0.10, and it seems Rails 3.1.1 did not like it at all. After starting up the server and hitting the homepage I got ArgumentError (dump format error (user class)). The simple fix was to change the cookie name used by the session store in config/initializers/session_store.rb as follows:

1 1  # Be sure to restart your server when you modify this file.
2 2 
3   -ProductionHub::Application.config.session_store :cookie_store, key: '_production_hub_session'
  3 +ProductionHub::Application.config.session_store :cookie_store, key: '_prohub_session'
4 4  
5 5  # Use the database for sessions instead of the cookie-based default,
6 6  # which shouldn't be used to store highly confidential information

Asset locations

A very simple gotcha that stumped me for quite a while was trying to figure out why my javascript_include_tag and stylesheet_link_tag calls stopped working. Well as it turns out, the pre-Rails-3.1 style of invoking these helpers with :all or :default doesn’t work with the asset pipeline.

First, I needed to re-locate my assets from public/javascripts to app/assets/javascripts and so on for images and stylesheets as well. In the process, I threw out jQuery files, since they would be provided by the jquery-rails gem, and I also moved some of the other ‘library’ scripts to vendor/assets. Then,

<%= javascript_include_tag :defaults %>

had to be changed to

<%= javascript_include_tag :application %>

And in app/assets/javascripts/application.js I had to add a manifest directive such as follows:

//= require jquery
//= require jquery-ui
//= require jquery_ujs
//= require jquery.timeentry
//= require_self
//= require_tree .

The same applied to the stylesheets, but eventually, this returned my ‘static’ assets to normal operation.

Route collisions

Another issue I ran into was a route naming collision with the asset pipeline. I have an Asset model in my application which is handled by Equipment::AssetsController. The controller is namespaced, but I had previously established the routes for this controller without the namespace, like so:

scope :module => "equipment", :path => "equipment" do
  resources :assets
end

This resulted in some routes like the following:

Eos:production-hub jcarlson$ rake routes | grep assets
    assets GET    /equipment/assets(.:format)           {:action=>"index", :controller=>"equipment/assets"}
           POST   /equipment/assets(.:format)           {:action=>"create", :controller=>"equipment/assets"}
 new_asset GET    /equipment/assets/new(.:format)       {:action=>"new", :controller=>"equipment/assets"}
edit_asset GET    /equipment/assets/:id/edit(.:format)  {:action=>"edit", :controller=>"equipment/assets"}
     asset GET    /equipment/assets/:id(.:format)       {:action=>"show", :controller=>"equipment/assets"}
           PUT    /equipment/assets/:id(.:format)       {:action=>"update", :controller=>"equipment/assets"}
           DELETE /equipment/assets/:id(.:format)       {:action=>"destroy", :controller=>"equipment/assets"}
Eos:production-hub jcarlson$

This worked fine before the asset pipeline, but now routes like asset_path conflicted with the asset pipeline, which also defines an asset_path helper. To resolve this, I conceded to namespace the resource routes:

namespace :equipment do
  resources :assets
end

And the resulting routes:

Eos:production-hub jcarlson$ rake routes | grep assets
    equipment_assets GET    /equipment/assets(.:format)           {:action=>"index", :controller=>"equipment/assets"}
                     POST   /equipment/assets(.:format)           {:action=>"create", :controller=>"equipment/assets"}
 new_equipment_asset GET    /equipment/assets/new(.:format)       {:action=>"new", :controller=>"equipment/assets"}
edit_equipment_asset GET    /equipment/assets/:id/edit(.:format)  {:action=>"edit", :controller=>"equipment/assets"}
     equipment_asset GET    /equipment/assets/:id(.:format)       {:action=>"show", :controller=>"equipment/assets"}
                     PUT    /equipment/assets/:id(.:format)       {:action=>"update", :controller=>"equipment/assets"}
                     DELETE /equipment/assets/:id(.:format)       {:action=>"destroy", :controller=>"equipment/assets"}
Eos:production-hub jcarlson$

I also had to update some of my views, but this took care of the naming conflict.

Parameter differences

Lastly, rake spec revealed some failures. It seems there is a slight difference in the way URL parameters are passed off to the controller, i.e., they are stringified before they hit the controller. This could be a side-effect of the testing framework, but I had to change this

# spec/controllers/clients_controller_spec.rb
it "should find the client by id" do
  Client.should_receive(:find).with(1)
  put :update, :id=> 1
end

in my controller spec to this

# spec/controllers/clients_controller_spec.rb
it "should find the client by id" do
  Client.should_receive(:find).with("1")
  put :update, :id=> 1
end

Deploying to Heroku

Finally, it looked like my application was running in Rails 3.1 with Ruby 1.9.2! I could begin testing on Heroku to iron out any last minute issues with deployment.

Eos:production-hub jcarlson$ git push staging release/rails-3.1.1:master

Heroku re-bundled the application to pickup Rails 3.1, detected and pre-compiled the assets automatically, and launched the application!

Cache-control

It’s worth noting that under the Cedar stack on Heroku, there is no Varnish HTTP cache in front of your application. Therefore, it is entirely up to the application to implement some form of caching mechanism.

Heroku always injects the rails3_serve_static_assets gem to ensure the application will serve static assets, like those precompiled by the asset pipeline. It’s a good idea, then, to enable static asset caching when serving an application on Heroku. in config/environments/production.rb, find the following:

config.serve_static_assets = false

and change it to

config.serve_static_assets = true
config.static_cache_control = "public, max-age=86400"

You can obviously customize the max-age directive to suit your needs, but this will get your application to apply some cache-control headers to static assets.

You can augment this behavior further by using a CDN like Amazon CloudFront to cache assets and configuring Rails to use an asset host, or add some Rack middleware to customize the cache headers, two things I’ll cover in a future post.

Conclusion

So in Part I, I upgraded my application to work with Ruby 1.9.2. In Part II, I updated the Heroku stack from bamboo-ree-1.8.7 to bamboo-mri-1.9.2 and then to cedar. And finally, in this, Part III, I completed the upgrade to Rails 3.1 and enabled the asset pipeline.

Deploying to production

I did all this using a staging environment to test as I go, so I am confident now that the changeover will be pretty painless when I flip the production version over to Rails 3.1.

However, I still need to tackle the task of converting my production version of the application over to Rails 3.1 on Cedar stack. Since I’ve already completed the coding and testing portions of that chore by doing so in a staging environment, my code is ready to deploy.

I’m not going to detail the production changeover, but the process will be an abbreviated version of the changeover from bamboo-mri-1.9.2 to cedar in the staging environment:

  1. Stand up a new application on Heroku with the cedar stack
  2. Configure the new application with production settings and add-ons
  3. Deploy the current release to the new application
  4. Switch the current live application into a ‘maintenance mode’ to stabilize the database
  5. Backup the database from the current live application and restore it to the new cedar application*
  6. Verify that the new application works correctly 7a. Rename both the current live application and the new application, if desired 7b. Update any DNS records to point to the new application

In all, the process should take no more than half an hour.

  • Alternatively, I could stand up the new application stack and configure its DATABASE_URL to match the value from the older application. In theory, both applications would be operating in parallel, accessing the same database. This would ensure a smooth transition when updating DNS. I’ve contacted Heroku support, though, to see what happens to the shared database if I destroy the owning application.

Leave a Reply

Your email address will not be published. Required fields are marked *