Save 37% off PRO during our Black Friday Sale! »

Living on the Rails Edge

Living on the Rails Edge

In this talk we would take a look in why keeping your application up-to-date is important and different strategies to upgrade Rails application to the newest version taking as example a huge monolithic Rails application. We will learn what were the biggest challenges and how they could be avoided.

0525b332aafb83307b32d9747a93de03?s=128

Rafael França

June 29, 2018
Tweet

Transcript

  1. Living on the Rails Edge

  2. None
  3. Rafael França rafaelfranca rafaelfranca

  4. Core Member

  5. None
  6. None
  7. None
  8. Why look at Shopify?

  9. Started around the same time as Rails

  10. None
  11. Never rewritten

  12. 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013

    2014 2015 2016 2017 2018 1.0 2.0 2.1 2.3 3.0 3.1 3.2 4.0 4.1 4.2 5.0 5.1 2.2 Initial commit Beginning of history 1.1 1.2 2.0 2.1 2.2 2.3 3.0 3.2 4.0 4.1 4.2 5.0 Rails Shopify 5.1 5.2 5.2
  13. It is a big application

  14. None
  15. It is in the latest Rails version

  16. Why live in the edge?

  17. BackgroundQueue

  18. # app/jobs/my_job.rb class MyJob include BackgroundQueue::Realtime def self.perform(name) # heavy

    lifting end end BackgroundQueue.push(MyJob, "rafael")
  19. https://www.mikeperham.com/2012/02/07/sidekiq-simple-efficient-messaging-for-rails/

  20. None
  21. None
  22. None
  23. https://weblog.rubyonrails.org/2014/8/20/Rails-4-2-beta1/

  24. class MyJob < ActiveJob::Base self.queue_adapter = :sidekiq queue_as :realtime def

    perform(name) # heavy lifting end end MyJob.perform_later("rafael")
  25. &'(

  26. Background Mail

  27. message = MyMailer.notify(user) # an ActionMailer::MessageDelivery object message.delivery # Will

    send the email
  28. message = MyMailer.notify(user) # an BackgroundMailing::MailerProxy object message.delivery # Will

    schedule a job to send the email
  29. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  30. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  31. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  32. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  33. class SendMailJob < BackgroundQueue::Job def perform(params) mailer_class = params[:mailer_class] mail_message

    = Marshal.load(Base64.decode64(params[:message])) method = params[:method] message = BackgroundMailing::MailerProxy.new(mailer_class: mailer_class.constantize, message: mail_message, method: method) message.deliver_now end end
  34. class SendMailJob < BackgroundQueue::Job def perform(params) mailer_class = params[:mailer_class] mail_message

    = params[:message] method = params[:method] message = BackgroundMailing::MailerProxy.new(mailer_class: mailer_class.constantize, message: mail_message, method: method) message.deliver_now end end
  35. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  36. None
  37. module BackgroundMailing class MailerProxy < Struct.new(:mailer_class, :message, :method) delegate_missing_to :message

    def initialize(mailer_class:, message:, method:) self.mailer_class = mailer_class self.message = message self.method = method end def deliver(queue: nil) marshal_encoded_message = Base64.encode64(Marshal.dump(message)) params = { queue: queue, mailer_class: mailer_class.to_s, message: marshal_encoded_message, method: method } BackgroundQueue.push(SendMailJob, params) message end def deliver_now mailer_class.wrap_delivery_behavior(message) message.deliver message end end end
  38. Marshal.dump(message)

  39. class Foo def initialize(value) @value = value end def value

    @value end end foo = Foo.new(1) # => #<Foo:0x00007ff59304f138 @value=1>o encoded = Marshal.dump(foo) # => "\x04\bo:\bFoo\x06:\v@valuei\x06" new_object = Marshal.load(encoded) # => #<Foo:0x00007ff59385cbf0 @value=1> new_object.value # => 1
  40. class Foo def initialize(value) @value = value end def value

    @value end end foo = Foo.new(1) # => #<Foo:0x00007ff59304f138 @value=1> encoded = Marshal.dump(foo) # => "\x04\bo:\bFoo\x06:\v@valuei\x06" new_object = Marshal.load(encoded) # => #<Foo:0x00007ff59385cbf0 @value=1> new_object.value # => 1
  41. class Foo def initialize(value) @value = value end def value

    @value end end foo = Foo.new(1) # => #<Foo:0x00007ff59304f138 @value=1> encoded = Marshal.dump(foo) # => "\x04\bo:\bFoo\x06:\v@valuei\x06" new_object = Marshal.load(encoded) # => #<Foo:0x00007ff59385cbf0 @value=1> new_object.value # => 1
  42. class Foo def initialize(value) @value = value end def value

    @value end end foo = Foo.new(1) # => #<Foo:0x00007ff59304f138 @value=1> encoded = Marshal.dump(foo) # => "\x04\bo:\bFoo\x06:\v@valuei\x06" new_object = Marshal.load(encoded) # => #<Foo:0x00007ff59385cbf0 @value=1> new_object.value # => 1
  43. None
  44. Mail::VERSION.version # => "2.5.4" mail = Mail.new do to 'rafael.franca@shopify.com'

    body 'Some simple body' end # => #<Mail::Message:70352211207020> message = Marshal.dump(mail) Mail::VERSION.version # => "2.6.0" mail = Marshal.load(message) # => ArgumentError (dump format error (user class))
  45. Mail::VERSION.version # => "2.5.4" mail = Mail.new do to 'rafael.franca@shopify.com'

    body 'Some simple body' end # => #<Mail::Message:70352211207020> message = Marshal.dump(mail) Mail::VERSION.version # => "2.6.0" mail = Marshal.load(message) # => ArgumentError (dump format error (user class))
  46. None
  47. https://weblog.rubyonrails.org/2014/8/20/Rails-4-2-beta1/

  48. module ActionMailer class MessageDelivery def deliver_later ActionMailer::DeliveryJob.perfor_later( @mailer_class.name, @action.to_s, “deliver_now",

    *@args ) end def deliver_now processed_mailer.handle_exceptions do message.deliver end end end end
  49. module ActionMailer class MessageDelivery def deliver_later ActionMailer::DeliveryJob.perfor_later( @mailer_class.name, @action.to_s, “deliver_now",

    *@args ) end def deliver_now processed_mailer.handle_exceptions do message.deliver end end end end
  50. module ActionMailer class MessageDelivery def deliver_later ActionMailer::DeliveryJob.perfor_later( @mailer_class.name, @action.to_s, “deliver_now",

    *@args ) end def deliver_now processed_mailer.handle_exceptions do message.deliver end end end end
  51. None
  52. None
  53. Why live in the edge?

  54. Build shared infrastructure

  55. Avoid snowflakes

  56. Get new features earlier

  57. Avoid regressions

  58. No more big bang upgrades

  59. How we can do it?

  60. Long-running branch strategy 1. Create a branch 2. Make all

    changes necessary 3. Merge it 4. ?????? 5. Profit!
  61. None
  62. Dual boot strategy

  63. $ BUNDLE_GEMFILE=Gemfile.next bundle install $ BUNDLE_GEMFILE=Gemfile bundle install

  64. Hack to share the same Gemfile

  65. if ENV['RAILS_NEXT'] # monkey patching to support dual booting module

    Bundler::SharedHelpers def default_lockfile=(path) @default_lockfile = path end def default_lockfile @default_lockfile ||= Pathname.new("#{default_gemfile}.lock") end end Bundler::SharedHelpers.default_lockfile = Pathname.new("#{Bundler::SharedHelpers.default_gemfile}_next.lock") # Bundler::Dsl.evaluate already called with an incorrect lockfile ... fix it class Bundler::Dsl # A bit messy, this can be called multiple times by bundler, avoid blowing the stack unless self.method_defined? :to_definition_unpatched alias_method :to_definition_unpatched, :to_definition end def to_definition(bad_lockfile, unlock) to_definition_unpatched(Bundler::SharedHelpers.default_lockfile, unlock) end end end if ENV['RAILS_NEXT'] gem 'rails', github: 'rails/rails', branch: '5-0-stable' else gem 'rails', '~> 4.2.7' end
  66. if ENV['RAILS_NEXT'] gem 'rails', github: 'rails/rails', branch: '5-2-stable' else gem

    'rails', '~> 5.1.5’ end
  67. $ RAILS_NEXT=1 bundle install $ bundle install

  68. Better way to share the same Gemfile

  69. Gemfile source 'https://rubygems.org' gem 'rails', '~> 5.0.0' eval_gemfile("Gemfile.shared")

  70. Gemfile.next source 'https://rubygems.org' gem 'rails', '~> 5.1.0.rc2' eval_gemfile("Gemfile.shared")

  71. Gemfile.shared gem 'sqlite3' gem 'puma', '~> 3.7' gem 'sass-rails', '~>

    5.0' gem 'uglifier', '>= 1.3.0' gem 'coffee-rails', '~> 4.2' gem 'turbolinks', '~> 5' gem 'jbuilder', '~> 2.5'
  72. config/boot.rb if ENV['RAILS_NEXT'] ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile.next', __dir__) else ENV['BUNDLE_GEMFILE'] ||=

    File.expand_path('../Gemfile', __dir__) end require 'bundler/setup' # Set up gems listed in the Gemfile.
  73. $ BUNDLE_GEMFILE=Gemfile.next bundle install $ BUNDLE_GEMFILE=Gemfile bundle install

  74. None
  75. Upgrade our dependencies

  76. Contribute to the dependencies that don’t support the latest Rails

    version
  77. Fix all the tests

  78. None
  79. Stop the bleeding

  80. mark_as :failing_on_rails_next test 'example test' do raise StandardError, 'This test

    fails but will be considered as passing' end
  81. /0123/4 124/04 01212424 /0123/4 124/04 01212424

  82. Componetization

  83. Before After

  84. None
  85. Road to production

  86. Compatibilities Layers

  87. None
  88. https://github.com/rails/rails/pull/26017

  89. Gradual rollout

  90. None
  91. Benchmarks

  92. None
  93. December 23th

  94. Cleanup

  95. Remove conditionals

  96. Remove deprecations

  97. class MyTest < MiniTest::Test def test_foo ActiveSupport::Deprecation.warn('This code is deprecated')

    ActiveSupport::Deprecation.warn('Another deprecation') end def test_bar ActiveSupport::Deprecation.warn('This code is deprecated') end end
  98. --- test_foo: - DEPRECATION WARNING: This code is deprecated -

    DEPRECATION WARNING: Another deprecation test_bar: - DEPRECATION WARNING: This code is deprecated
  99. Deprecation Toolkit

  100. Preparation to the next upgrade

  101. Keep going

  102. None
  103. Avoid monkey patches

  104. Keep dependencies number small

  105. Own your dependencies

  106. Keep the parallel CI

  107. Think about backwards compatibility

  108. Make it everyone's concern

  109. Don’t give up

  110. Rafael França rafaelfranca rafaelfranca Thank you