Slide 1

Slide 1 text

UPGRADING GITHUB from Ruby 2.6 to 2.7 a Eileen M. Uchitelle | @eileencodes

Slide 2

Slide 2 text

Hello! I’m Eileen M. Uchitelle Principal Engineer at GitHub Rails Core Team Find me: @eileencodes

Slide 3

Slide 3 text

UPGRADING GITHUB from Ruby 2.6 to 2.7 Eileen M Uchitelle | @eileencodes a

Slide 4

Slide 4 text

GitHub is born 2007 2008 2009 Forked Rails & Ruby Public launch

Slide 5

Slide 5 text

a

Slide 6

Slide 6 text

a Why was this Ruby upgrade hard?

Slide 7

Slide 7 text

a Running GitHub Deprecation Free

Slide 8

Slide 8 text

a

Slide 9

Slide 9 text

a Over 11k deprecation warnings!

Slide 10

Slide 10 text

DEPRECATION: Separation of positional and keyword arguments

Slide 11

Slide 11 text

class MyClass def initialize(part_1:, part_2:) puts "#{part_1} #{part_2}" end end DEPRECATION: Separation of arguments

Slide 12

Slide 12 text

MyClass.new({ part_1: "I work fine", part_2: "in Ruby 2.6" }) => "I work fine in Ruby 2.6" DEPRECATION: Separation of arguments

Slide 13

Slide 13 text

MyClass.new({ part_1: "I throw warnings", part_2: "in 2.7" }) => "I throw warnings in 2.7" example.rb:13: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call example.rb:2: warning: The called method `initialize' is defined here DEPRECATION: Separation of arguments

Slide 14

Slide 14 text

DEPRECATION: Separation of arguments MyClass.new({ part_1: "I throw warnings", part_2: "in 2.7" }) => "I throw warnings in 2.7" example.rb:13: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call example.rb:2: warning: The called method `initialize' is defined here The Caller

Slide 15

Slide 15 text

DEPRECATION: Separation of arguments MyClass.new({ part_1: "I throw warnings", part_2: "in 2.7" }) => "I throw warnings in 2.7" example.rb:13: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call example.rb:2: warning: The called method `initialize' is defined here The Definition

Slide 16

Slide 16 text

MyClass.new({ part_1: "I throw warnings", part_2: "in 2.7" }) MyClass.new( part_1: "I don't throw warnings", part_2: "in 2.7" ) => "I don't throw warnings in 2.7" DEPRECATION: Separation of arguments

Slide 17

Slide 17 text

MyClass.new({ part_1: "I throw warnings", part_2: "in 2.7" }) MyClass.new(**{ part_1: "I don't throw warnings", part_2: "in 2.7" }) => "I don't throw warnings in 2.7" DEPRECATION: Separation of arguments

Slide 18

Slide 18 text

class MyClass def initialize(part_1:, part_2:) puts "#{part_1} #{part_2}" end end class AbstractClass def initialize(*args) MyClass.new(*args) end end DEPRECATION: Separation of arguments

Slide 19

Slide 19 text

AbstractClass.new( part_1: "I throw warnings", part_2: "in 2.7" ) DEPRECATION: Separation of arguments

Slide 20

Slide 20 text

AbstractClass.new( part_1: "I throw warnings", part_2: "in 2.7" ) example.rb:9: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call example.rb:2: warning: The called method `initialize' is defined here DEPRECATION: Separation of arguments

Slide 21

Slide 21 text

class MyClass def initialize(part_1:, part_2:) puts "#{part_1} #{part_2}" end end class AbstractClass def initialize(**kwargs) MyClass.new(**kwargs) end end DEPRECATION: Separation of arguments

Slide 22

Slide 22 text

class MyClassJob def perform(part_1:, part_2:) puts "#{part_1} #{part_2}" end end DEPRECATION: Separation of arguments

Slide 23

Slide 23 text

module ActiveJob module Enqueuing def perform_later(*args) job_or_instantiate(*args).enqueue end end end DEPRECATION: Separation of arguments

Slide 24

Slide 24 text

module ActiveJob module Enqueuing def perform_later(*args) job_or_instantiate(*args).enqueue end ruby2_keywords(:perform_later) if respond_to?(:ruby2_keywords, true) end end DEPRECATION: Separation of arguments

Slide 25

Slide 25 text

MyClassJob.perform_later({ part_1: "I am a", part_2: "job with kwargs" }) activejob/lib/active_job/execution.rb:48: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call app/jobs/my_job_class_job.rb:2: warning: The called method `perform' is defined here DEPRECATION: Separation of arguments

Slide 26

Slide 26 text

DEPRECATION: URI method deprecations

Slide 27

Slide 27 text

DEPRECATION: URI Methods URI.encode URI.decode URI.escape URI.unescape

Slide 28

Slide 28 text

URI.escape("https://google.com?query_param=1") => "https://google.com?query_param=1" uri_example.rb:1: warning: URI.escape is obsolete DEPRECATION: URI Methods

Slide 29

Slide 29 text

URI.escape("https://google.com?query_param=1") Addressable::URI.escape("https://google.com? query_param=1") => "https://google.com?query_param=1" DEPRECATION: URI Methods

Slide 30

Slide 30 text

URI.escape("https://goog\nle.com?query_param=1") => "https://goog%0Ale.com?query_param=1" Addressable::URI.escape("https://goog\nle.com? query_param=1") => Addressable::URI::InvalidURIError (Invalid character in host: 'goog) le.com' DEPRECATION: URI Methods

Slide 31

Slide 31 text

URI.escape("https://goog\nle.com?query_param=1") => "https://goog%0Ale.com?query_param=1" CGI.escape("https://goog\nle.com?query_param=1") => "https%3A%2F%2Fgoog%0Ale.com%3Fquery_param%3D1" DEPRECATION: URI Methods

Slide 32

Slide 32 text

a How to Fix More Than 11k Deprecations

Slide 33

Slide 33 text

a UPGRADING: Dual-booting our application

Slide 34

Slide 34 text

# config/ruby-version RUBY_NEXT_SHA = "d1ba554" RUBY_SHA = "dcc231c" if ENV["RUBY_NEXT"] print RUBY_NEXT_SHA else print RUBY_SHA end UPGRADING: Dual-booting

Slide 35

Slide 35 text

RUBY_NEXT=1 bin/rails server RUBY_NEXT=1 bin/rails console RUBY_NEXT=1 bin/rails test path/to/test_file.rb UPGRADING: Dual-booting

Slide 36

Slide 36 text

UPGRADING: Dual-booting

Slide 37

Slide 37 text

UPGRADING: Dual-booting

Slide 38

Slide 38 text

a UPGRADING: Monkey patch Warning module

Slide 39

Slide 39 text

module Warning def self.warn(message) STDERR.print(message) if ENV["RAISE_ON_WARNINGS"] raise message end if ENV["DEBUG_WARNINGS"] STDERR.puts caller end end end UPGRADING: Warning monkey patch

Slide 40

Slide 40 text

module Warning def self.warn(message) STDERR.print(message) if ENV["RAISE_ON_WARNINGS"] raise message end if ENV["DEBUG_WARNINGS"] STDERR.puts caller end end end UPGRADING: Warning monkey patch

Slide 41

Slide 41 text

module Warning def self.warn(message) STDERR.print(message) if ENV["RAISE_ON_WARNINGS"] raise message end if ENV["DEBUG_WARNINGS"] STDERR.puts caller end end end UPGRADING: Warning monkey patch

Slide 42

Slide 42 text

module Warning def self.warn(message) STDERR.print(message) if ENV["RAISE_ON_WARNINGS"] raise message end if ENV["DEBUG_WARNINGS"] STDERR.puts caller end end end UPGRADING: Warning monkey patch

Slide 43

Slide 43 text

activejob/lib/active_job/execution.rb:48: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call activejob/lib/active_job/execution.rb:48:in `block in perform_now' ... test/integration/a_test.rb:6:in `block in test_something' activesupport/lib/active_support/testing/assertions.rb:34:in `assert_nothing_raised' activejob/lib/active_job/test_helper.rb:591:in `perform_enqueued_jobs' test/integration/my_class_job_test.rb:5:in `test_my_class_job' ... app/jobs/my_job_class_job.rb:2: warning: The called method `perform' is defined here UPGRADING: Warning monkey patch

Slide 44

Slide 44 text

def test_my_class_job perform_enqueued_jobs do MyClassJob.perform_later({ part_1: "I am a", part_2: "job with kwargs" }) end ... end UPGRADING: Warning monkey patch

Slide 45

Slide 45 text

module Warning def self.warn(message) line = caller_locations.find do |location| location.path.end_with?("_test.rb") end WarningsCollector.instance << [message.chomp, line.path] STDERR.print(message) ... UPGRADING: Warning monkey patch

Slide 46

Slide 46 text

class WarningsCollector < ParallelCollector def process path = File.join("/tmp", "warnings.txt") File.open(path, "a") do |f| @data.each do |message, origin| f.puts [message, origin].join("*^.^*") end end script = File.absolute_path( "../../../script/process-warnings", __FILE__) system(script, "/tmp") end end UPGRADING: Warning monkey patch

Slide 47

Slide 47 text

class WarningsCollector < ParallelCollector def process path = File.join("/tmp", "warnings.txt") File.open(path, "a") do |f| @data.each do |message, filepath| f.puts [message, filepath].join("*^.^*") end end script = File.absolute_path( "../../../script/process-warnings", __FILE__) system(script, "/tmp") end end UPGRADING: Warning monkey patch

Slide 48

Slide 48 text

class WarningsCollector < ParallelCollector def process path = File.join("/tmp", "warnings.txt") File.open(path, "a") do |f| @data.each do |message, filepath| f.puts [message, filepath].join("*^.^*") end end script = File.absolute_path( "../../../script/process-warnings", __FILE__) system(script, "/tmp") end end UPGRADING: Warning monkey patch

Slide 49

Slide 49 text

class WarningsCollector < ParallelCollector def process path = File.join("/tmp", "warnings.txt") File.open(path, "a") do |f| @data.each do |message, filepath| f.puts [message, filepath].join("*^.^*") end end script = File.absolute_path( "../../../script/process-warnings", __FILE__) system(script, "/tmp") end end UPGRADING: Warning monkey patch

Slide 50

Slide 50 text

warnings = {} Dir["tmp/warning*.txt"].each do |file| File.read(file).split("\n").each do |filepath| message, filepath = line.split("*^.^*") warnings[message] ||= Message.new(message) warnings[message].paths << filepath if filepath end end warnings.values.each do |warning| warning.owner ||= CODEOWNERS.for(source_file).keys.first.to_s end UPGRADING: Warning monkey patch

Slide 51

Slide 51 text

- [ ] `test/lib/platform/mutations/ update_mobile_push_notification_schedules_test.rb` - **warnings** - Line 118: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call - [ ] `test/test_helpers/newsies/deliver_notifications_job_test_helper.rb` - **warnings** - Line 1282: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call - Line 1283: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call - **test suites that trigger these warnings** UPGRADING: Warning File Generation

Slide 52

Slide 52 text

UPGRADING: Warning monkey patch

Slide 53

Slide 53 text

UPGRADING: Warning File Generation

Slide 54

Slide 54 text

a UPGRADING: Fixing warnings in gems

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

a UPGRADING Replace abandoned gems

Slide 57

Slide 57 text

a UPGRADING Preventing regressions

Slide 58

Slide 58 text

a Our Deploy Process

Slide 59

Slide 59 text

a DEPLOYING: Make smaller change sets

Slide 60

Slide 60 text

a DEPLOYING: Testing on staging

Slide 61

Slide 61 text

a

Slide 62

Slide 62 text

a DEPLOYING: Slow & incremental rollout

Slide 63

Slide 63 text

2% Incremental rollout

Slide 64

Slide 64 text

2% Incremental rollout

Slide 65

Slide 65 text

2% 0% Incremental rollout

Slide 66

Slide 66 text

2% 0% 2% Incremental rollout

Slide 67

Slide 67 text

2% 0% 2% 30% Incremental rollout

Slide 68

Slide 68 text

2% 0% 2% 30% 60% Incremental rollout

Slide 69

Slide 69 text

2% 0% 2% 30% 60% 30% Incremental rollout

Slide 70

Slide 70 text

2% 0% 2% 30% 60% 30% 100% Incremental rollout

Slide 71

Slide 71 text

a DEPLOYING: Strive for boring deploys

Slide 72

Slide 72 text

a Features Worth Upgrading For

Slide 73

Slide 73 text

FEATURE: Performance improvements

Slide 74

Slide 74 text

FEATURES: Boot-time decrease

Slide 75

Slide 75 text

FEATURES: Boot-time decrease

Slide 76

Slide 76 text

FEATURES: Allocations decrease

Slide 77

Slide 77 text

FEATURE: Method#inspect improvements

Slide 78

Slide 78 text

class MyClass ... def my_method(arg, part_1:) end end obj = MyClass.new(part_1: "a", part_2: "b") obj.method(:my_method) => # FEATURE: Method#inspect improvements

Slide 79

Slide 79 text

class MyClass ... def my_method(arg, part_1:) end end obj = MyClass.new(part_1: "a", part_2: "b") obj.method(:my_method) => # FEATURE: Method#inspect improvements

Slide 80

Slide 80 text

FEATURE: IRB Improvements

Slide 81

Slide 81 text

FEATURE: REPL Improvements

Slide 82

Slide 82 text

FEATURE: Manual GC Compaction

Slide 83

Slide 83 text

GC.compact FEATURE: Method#inspect improvements

Slide 84

Slide 84 text

a Making Ruby (Even) Better

Slide 85

Slide 85 text

If you don't like how Ruby works, change it.

Slide 86

Slide 86 text

UPSTREAM FIX: Warning categories

Slide 87

Slide 87 text

Warning[:deprecated] = false UPSTREAM FIX: Warning categories

Slide 88

Slide 88 text

UPSTREAM FIX: Warning categories

Slide 89

Slide 89 text

module Warning def self.warn(message, category: nil) if category == :deprecated raise message else super end end end UPSTREAM FIX: Warning categories

Slide 90

Slide 90 text

a Why You Should Upgrade (like yesterday)

Slide 91

Slide 91 text

a Nothing makes an upgrade harder than waiting

Slide 92

Slide 92 text

Thank You! Eileen M. Uchitelle Principal Engineer at GitHub Rails Core Team Find me: @eileencodes