Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Upgrading to Rails 3 on an active master branch

Upgrading to Rails 3 on an active master branch

Changing the wheels on the bus at 80 mph
Andrew Bloomgarden and Julian Giuca
RailsConf 2013

Long-running branches are painful, but upgrading to Rails 3 requires one if you can't stop development, right? Wrong! At New Relic, we worked on upgrading to Rails 3 on master while letting development continue in Rails 2. We patched Bundler, built a backwards-compatible boot sequence, and punched ActiveScaffold in the face. Other developers, meanwhile, released 1400 commits worth of work without noticing any changes. We talk about what we did, why we did it, and why we think this approach can help developers get over the hurdle into the Rails 3 promised land.

Andrew Bloomgarden

April 29, 2013
Tweet

Other Decks in Technology

Transcript

  1. UPGRADING TO RAILS 3 Changing the wheels on the bus

    at 80mph (or 128.784 kph for everyone else) Andrew Bloomgarden Julian Giuca
  2. 4

  3. 2.2.2# 2.2.3# 2.3.2# 2.3.4# 2.3.5# 2.3.8# 2.3.10# 2.3.11# 2.3.12# 2.3.14#

    2.3.15# 3.0.1# 3.0.3# 3.0.5# 3.0.11# 3.0.15# 3.0.18# 3.0.19# 3.1.1# 3.2.1# 3.2.8# 3.2.11# 3.2.13# Rails versions with >1 server 2.x.x 3.x.x Number of accounts
  4. Upgrading to Rails 3 1. Procrastinate 2. Goals 3. Feature

    Branch 4. Fix tests 5. Merge 6. Push 7. Cross fingers 8. Fix Production 11
  5. Putting it off Rearchitecting the app first Do it all

    in a big spike! Have a ruby_19 branch STUPID THINGS WE TRIED 12
  6. Rails 3? Why now? Someone will make upgrading easier soon!

    Oh man, this is going to be awful. 13
  7. 16

  8. Upgrading to Rails 3 1. Procrastinate 2. Goals 3. Feature

    Branch 4. Fix tests 5. Merge 6. Push 7. Cross fingers 8. Fix Production 17
  9. Keep ourselves happy CONFLICT (content): … CONFLICT (content): … CONFLICT

    (content): … ⋮ CONFLICT (content): … Automatic merge failed; fix conflicts and then commit the result. 21
  10. Upgrading to Rails 3 1. Procrastinate 2. Goals 3. Feature

    Branch 4. Fix tests 5. Merge 6. Push 7. Cross fingers 8. Fix Production 22
  11. 23

  12. 24

  13. 27

  14. In the Gemfile if ENV.include?("USE_RAILS_3") class Bundler::Dsl if !self.method_defined?(:to_definition_without_rails3_lockfile) alias_method

    :to_definition_without_rails3_lockfile, :to_definition end def to_definition(old_lockfile, unlock) current = File.expand_path(Dir.pwd) filename = File.join(current, "Gemfile_rails3.lock") lockfile = Pathname.new(filename) to_definition_without_rails3_lockfile(lockfile, unlock) end end module Bundler::SharedHelpers def default_lockfile current = File.expand_path(Dir.pwd) filename = File.join(current, "Gemfile_rails3.lock") lockfile = Pathname.new(filename) Pathname.new(lockfile) end end end 28
  15. Gemfile if is_rails3 gem 'rails', '3.0.19' gem 'lighthouse-api', '~>2.0.0' gem

    'dalli', '~> 2.3.0' gem 'jquery-rails', '~>2.1' gem 'active_reload' else gem 'rails', '2.3.15' gem 'lighthouse-api', '~>1.1.0' gem 'dalli', '~> 1.0.4' end 30
  16. Upgrading to Rails 3 1. Procrastinate 2. Goals 3. Feature

    Branch 4. Fix tests 5. Merge 6. Push 7. Cross fingers 8. Fix Production 32
  17. Upgrading to Rails 3 1. Procrastinate 2. Goals 3. Feature

    Branch 4. Fix tests 5. Merge 6. Push 7. Cross fingers 8. Fix Production Improved! 32
  18. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 33
  19. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 34
  20. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 36
  21. Onto master 1. Boot Rails 3 2. Dual boot 3.

    Unbreak Rails 2 4. Merge upstream 37
  22. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3
  23. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3
  24. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3
  25. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3 Dual boot
  26. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3 Dual boot
  27. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3 Dual boot
  28. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3 Dual boot Restore Rails 2
  29. Unbreak Rails 2 Boots Rails 2 Runs Rails 2 Boots

    Rails 3 Runs Rails 3 Rails 2 Boot Rails 3 Dual boot Restore Rails 2
  30. Onto master 1. Boot Rails 3 2. Dual boot 3.

    Unbreak Rails 2 4. Merge upstream 39
  31. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 40
  32. Dual-booting that works Rails 2.3 Rails 3 config/boot.rb requires Rails

    requires Bundler config/environment.rb loads, configures, and initializes the app initializes the app config/application.rb doesn’t exist loads and configures the app 41
  33. Dual-booting that works Rails 2.3 Rails 3 config/boot.rb requires Rails

    requires Bundler config/environment.rb loads, configures, and initializes the app initializes the app config/application.rb doesn’t exist loads and configures the app 41
  34. config/boot.rb if ENV.include?("USE_RAILS_3") require 'rubygems' # Set up gems listed

    in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' 43 else # 123 lines that load Rails # All that for this: Rails.boot! end
  35. config/boot.rb if ENV.include?("USE_RAILS_3") require 'rubygems' # Set up gems listed

    in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' 43 else # 123 lines that load Rails # All that for this: Rails.boot! end
  36. config/boot.rb if ENV.include?("USE_RAILS_3") require 'rubygems' # Set up gems listed

    in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' 43 else # 123 lines that load Rails # All that for this: Rails.boot! end
  37. config/environment.rb # Load the rails application require_relative "application" if CoreAppConfig.is_rails3?

    # Initialize the rails application RpmSite::Application.initialize! end 44
  38. config/application.rb module NewRelicApplicationConfig def self.rails3_config(config) # Do Rails 3 stuff

    common_config(config) end def self.rails2_config(config) # Do Rails 2 stuff common_config(config) end def self.common_config(config) # ... end end 45
  39. config/application.rb 46 if is_rails3 require 'rails/all' Bundler.require(:default, Rails.env) module RpmSite

    class Application < Rails::Application ::NewRelicApplicationConfig.rails3_config(config) end end else Rails::Initializer.run do |config| ::NewRelicApplicationConfig.rails2_config(config) end end
  40. config/application.rb 46 if is_rails3 require 'rails/all' Bundler.require(:default, Rails.env) module RpmSite

    class Application < Rails::Application ::NewRelicApplicationConfig.rails3_config(config) end end else Rails::Initializer.run do |config| ::NewRelicApplicationConfig.rails2_config(config) end end
  41. config/application.rb 46 if is_rails3 require 'rails/all' Bundler.require(:default, Rails.env) module RpmSite

    class Application < Rails::Application ::NewRelicApplicationConfig.rails3_config(config) end end else Rails::Initializer.run do |config| ::NewRelicApplicationConfig.rails2_config(config) end end
  42. Never again def getopts opts = {} if RUBY_VERSION.to_f >=

    1.9 # In 1.9 this throw/catch/getopts nonsense breaks lambda do |*args| keys, default, ignored = args [keys].flatten.each do |key| [key, key.to_s, key.to_s.intern].each do |key| return opts[key] if opts.has_key?(key) end end return default end else # In 1.8 you can't return across threads lambda do |*args| keys, default, ignored = args catch('opt') do [keys].flatten.each do |key| [key, key.to_s, key.to_s.intern].each do |key| throw 'opt', opts[key] if opts.has_key?(key) end end default end end end end 51
  43. Use Resque or BackgroundJob class Async::Command < Async::Job if CoreAppConfig.use_resque?

    include Async::ResqueJob extend Resque::Plugins::History else include Async::BjJob end # ... end 53
  44. Branch by abstraction 55 The problem being with feature branches

    is that the current state of any one of them might be unable to be deployed for a number of weeks while the team gets it right. Those branches just end up running and running …. – Paul Hammant
  45. Branch by abstraction 55 Branch by abstraction: a pattern for

    making large-scale changes to your application incrementally on mainline. – Jez Humble
  46. Your old mailers still work class AccountMailer < ApplicationMailer def

    new_account(account, user, subscription) headers['Errors-to'] = BOUNCE_ACCOUNT from BILLING_ACCOUNT subject "New Relic invoice account request" recipients INVOICE_RECIPIENT @account = account @user = user @subscription = subscription end end 59
  47. Your old mailers still work class AccountMailer < ApplicationMailer def

    new_account(account, user, subscription) headers['Errors-to'] = BOUNCE_ACCOUNT from BILLING_ACCOUNT subject "New Relic invoice account request" recipients INVOICE_RECIPIENT @account = account @user = user @subscription = subscription end end 59
  48. Your error handling (can) still work # included into ApplicationController

    def rescue_with_handler(e) if super # ActiveSupport::Rescuable true else rescue_action_in_public e true end end 62
  49. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 64
  50. We want YOU to test! 65 To run in Rails

    3, just run script/server3
  51. 74

  52. Punching ActiveScaffold in the face .../bundle/ruby/1.9.1/gems/ active_scaffold-3.0.26/lib/ active_scaffold/bridges/date_picker/ lib/datepicker_bridge.rb:127:in `binread':

    No such file or directory - .../public/ javascripts/active_scaffold/default/ date_picker_bridge.js (Errno::ENOENT) 79
  53. From an idea to production 1. Procrastinate 2. Goals 3.

    Onto master 4. Getting it to work 5. Rollout 6. Lessons learned 83
  54. Lessons learned 1. Preorder the donuts your on-call engineers like

    2. Deprecate ActiveScaffold 3. Schedule lots of time for firefighting and cleanup 84
  55. •Get it into master as early as possible •Incremental everything

    •CI is your bestest childhood friend (and you secretly have a crush on them) The special magic pony sauce 85