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.
39. 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
60. From an idea to production
1. Procrastinate
2. Goals
3. Onto master
4. Getting it to work
5. Rollout
6. Lessons learned
40
61. 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
62. 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
64. 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
65. 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
66. 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
67. config/environment.rb
# Load the rails application
require_relative "application"
if CoreAppConfig.is_rails3?
# Initialize the rails application
RpmSite::Application.initialize!
end
44
76. 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
78. Add an abstraction layer
Async::Command.new(
'script/background/do_something.rb'
).notify(DEV_TEAM).enqueue
52
79. 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
81. 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
82. Branch by abstraction
55
Branch by abstraction: a pattern
for making large-scale changes to your
application incrementally on mainline.
– Jez Humble
85. Your old routes still work
RpmSite::Application.routes.draw do |map|
Jammit::Routes.draw(map)
# ...
end
58
86. Your old routes still work
RpmSite::Application.routes.draw do |map|
Jammit::Routes.draw(map)
# ...
end
58
87. 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
88. 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
89. Your old models (can) still work
self.store_full_sti_class = false
60
90. Your old views (can) still work
helper PrototypeHelper
clear_helpers
61
91. 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
108. Breaking the audit trail
ActionController::Base.class_eval do
cache_sweeper :audit_sweeper
end
Audit.add_observer(AuditSweeper.instance)
class AuditSweeper < ActionController::Caching::Sweeper
observe Audit
end
77
110. 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
114. From an idea to production
1. Procrastinate
2. Goals
3. Onto master
4. Getting it to work
5. Rollout
6. Lessons learned
83
115. Lessons learned
1. Preorder the donuts your on-call engineers like
2. Deprecate ActiveScaffold
3. Schedule lots of time for firefighting and cleanup
84
116. •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