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

Ruby 3.0 & Rails 6.1

Ruby 3.0 & Rails 6.1

A walkthrough of the biggest changes in Ruby 3.0 and Rails 6.1

F77032adcbe77d2777bb0e0c30873159?s=128

Ernesto Tagwerker

January 26, 2021
Tweet

Transcript

  1. Ruby 3.0 & Rails 6.1 Ernesto Tagwerker (@etagwerker) January 28th,

    2021
  2. Hi, I’m from Argentina I live in Philadelphia I ❤

    Open Source
  3. Heroes for Hire Alum

  4. Founder & Software Engineer @FastRubyIO & @OmbuLabs

  5. Sample Code $ git clone git@github.com:etagwerker/ create-2021.git

  6. Ruby 3.0

  7. - Ractor - Pattern Matching - Ruby 3x3 - Memory:

    Garbage Compaction - Fiber Scheduler - Static Analysis - JIT Improvements - And more…
  8. Ruby 3.0 (w/ rvm) > rvm get head > rvm

    install 3.0 > rvm use 3.0 > ruby --version ruby 3.0.0p0 (2020-12-25 revision 95aff21468) [x86_64-darwin19]
  9. Rightward Assignments 1 42 => answer 2 p answer #=>

    42 3 4 {first: "John", last: "Doe"} => {first:,last:} 5 p first #=> "John" 6 p last #=> "Doe"
  10. Find Pattern 1 case ["a", 1, "b", "c", 2, "d",

    "e", "f", 3] 2 in [*pre, String => x, String => y, *post] 3 p pre #=> ["a", 1] 4 p x #=> "b" 5 p y #=> "c" 6 p post #=> [2, "d", "e", "f", 3] 7 end
  11. Endless Method Definitions 1 def square(x) = x * x

    2 3 puts "7 square = #{square(7)}"
  12. Ractor • The feature formerly known as “guilds”.

  13. Ractor • Ractor is an Actor-model like concurrent abstraction.

  14. Concurrency vs. Parallelism

  15. Concurrency vs. Parallelism Fibers Threads Ractors

  16. Ractor • Ractor is designed to provide a parallel execution

    feature of Ruby without thread-safety concerns.
  17. Ractor • Ractor does not use the GVL. Each ractor

    has its own GVL, so you can have multiple threads within your ractor.
  18. What is the GVL? (Global VM Lock) • “The Ruby

    Virtual Machine is not internally thread-safe, […] so we use a global lock around it so that only one thread can access it in parallel.” Nate Berkopec
  19. A typical thread

  20. Parallelism Concerns • Non-determinism • Race conditions • Deadlocks •

    And more...
  21. Ractor by design: “… without thread-safety concerns” • Shareable objects

    • Unshareable objects
  22. Threads (This is OK) 1 GOOD = 'good'.freeze 2 BAD

    = 'bad' 3 4 t = Thread.new do 5 puts "GOOD=#{GOOD}" 6 puts "BAD=#{BAD}" 7 end 8 9 t.join GOOD=good BAD=bad
  23. 1 GOOD = 'good'.freeze 2 BAD = 'bad' 3 4

    r = Fiber.new do 5 puts "GOOD=#{GOOD}" 6 puts "BAD=#{BAD}" 7 end 8 9 r.resume GOOD=good BAD=bad Fibers (This is OK)
  24. 1 GOOD = 'good'.freeze 2 BAD = 'bad' 3 4

    r = Ractor.new do 5 puts "GOOD=#{GOOD}" 6 puts "BAD=#{BAD}" 7 end 8 9 r.take Ractor (This is NOT OK)
  25. <internal:ractor>:267: warning: Ractor is experimental, and the behavior may change

    in future versions of Ruby! Also there are many implementation issues. GOOD=good #<Thread:0x00007ffc6b942310 run> terminated with exception (report_on_exception is true): constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) <internal:ractor>:694:in `take': thrown by remote Ractor. (Ractor::RemoteError) from constants_with_ractor.rb:9:in `<main>' constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) Ractor
  26. <internal:ractor>:267: warning: Ractor is experimental, and the behavior may change

    in future versions of Ruby! Also there are many implementation issues. GOOD=good #<Thread:0x00007ffc6b942310 run> terminated with exception (report_on_exception is true): constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) <internal:ractor>:694:in `take': thrown by remote Ractor. (Ractor::RemoteError) from constants_with_ractor.rb:9:in `<main>' constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) Ractor
  27. <internal:ractor>:267: warning: Ractor is experimental, and the behavior may change

    in future versions of Ruby! Also there are many implementation issues. GOOD=good #<Thread:0x00007ffc6b942310 run> terminated with exception (report_on_exception is true): constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) <internal:ractor>:694:in `take': thrown by remote Ractor. (Ractor::RemoteError) from constants_with_ractor.rb:9:in `<main>' constants_with_ractor.rb:6:in `block in <main>': can not access non-shareable objects in constant Object::BAD by non-main Ractor. (Ractor::IsolationError) Ractor
  28. When and why might Ractors be better than existing Ruby

    threads? • “The lack of GVL allows them to fully use more cores. So in cases where Ruby can't easily use all your cores, Ractors can be better than threads.” Noah Gibbs
  29. Ractor: Shareable vs. Unshareable Objects • Shareable objects: • Immutable

    objects (frozen objects only refer to shareable objects) • Class/module objects • Special shareable objects (Ractor objects, and so on) • Unshareable objects: Everything else
  30. Pattern Matching • You might be familiar with the case/when

    flow: 1 object = "a string" 2 3 case object 4 when String 5 puts "object is a String" 6 else 7 puts "object is not a String" 8 end $ ruby case_when.rb object is a String
  31. Pattern Matching • Ruby 3.0 introduces the case/in flow which

    uses pattern matching: 1 # example based on https://docs.ruby-lang. 2 org/en/3.0.0/doc/syntax/pattern_matching_rdoc. 3 html 4 5 def connect(config) 6 case config 7 in db: {user:} # matches subhash and puts 8 matched value in variable user 9 puts "Connect with user '#{user}'" 10 in connection: {username: } 11 puts "Connect with connection and user '#{username}'" 12 else 13 puts "Unrecognized structure of config" 14 end 15 end 16 17 connect(db: {user: 'admin', password: ‘abc123'}) $ ruby case_in.rb Connect with user 'admin'
  32. • any Ruby object (matched by the === operator, like

    in when); (Value pattern) • array pattern: [<subpattern>, <subpattern>, <subpattern>, ...]; (Array pattern) • find pattern: [*variable, <subpattern>, <subpattern>, <subpattern>, ..., *variable]; (Find pattern) • hash pattern: {key: <subpattern>, key: <subpattern>, ...}; (Hash pattern) • combination of patterns with |; (Alternative pattern) • variable capture: <pattern> => variable or variable; (As pattern, Variable pattern) Patterns can be:
  33. Variable Binding 1 case [17, 76] 2 in Integer =>

    a, Integer => b 3 puts "a=#{a}, b= #{b}" 4 else 5 puts "not matched" 6 end 7 8 case {a: 1, b: 7, c: 76} 9 in a: Integer => m, b: Integer => n, c: Integer => o 10 puts "m: #{m}, n: #{n}, o: #{o}, " 11 else 12 "not matched" 13 end $ ruby binding.rb a=17, b= 76 m: 1, n: 7, o: 76,
  34. You can even use it in an if as a

    “one-liner”: 1 first_name = "John" 2 3 if {first_name: first_name} in {first_name: "John"} 4 5 puts "First name is John!" 6 end 7 8 if {first_name: first_name} in {first_name: "Bob"} 9 10 puts "First name is Bob!" 11 else 12 puts "First name is not Bob!" 13 end if_in.rb:3: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby! if_in.rb:7: warning: One-line pattern matching is experimental, and the behavior may change in future versions of Ruby! First name is John! First name is not Bob!
  35. If you want to skip the warnings: 1 Warning[:experimental] =

    false
  36. Pattern Matching: Advanced Example

  37. Pattern Matching: Poker Hand

  38. Elixir-like Method Overloading Not Supported: 1 def process(%{"animal" => animal})

    do 2 IO.puts("The animal is: #{animal}") 3 end 4 5 def process(%{"plant" => plant}) do 6 IO.puts("The plant is: #{plant}") 7 end 8 9 def process(%{"person" => person}) do 10 IO.puts("The person is: #{person}") 11 end •
  39. Ruby 3x3 • “The goal is to make Ruby 3

    run three times faster as compared to Ruby 2.0.” * Matz, 2016 (source)
  40. Ruby 3x3

  41. What is a JIT? • JIT stands for “just-in-time” compilation.

    A JIT allows an interpreted language such as Ruby to optimize frequently run methods so they run faster for future calls.
  42. Ruby’s MJIT • The M is for methods • It

    requires more memory to keep its method operations cache • It has been available since Ruby 2.6 and it has been optimized on Ruby 3.0
  43. JIT: How do I use it? # enabled.rb 1 puts

    "MJIT is enabled: #{RubyVM::MJIT.enabled?}” $ ruby --jit enabled.rb MJIT is enabled: true $ ruby enabled.rb MJIT is enabled: false
  44. JIT: How do I use it in Rails? $ RUBYOPT=--jit

    rails server
  45. Ruby 3x3

  46. Ruby 3x3

  47. Memory: Automatic Garbage Compaction • You no longer have to

    manually trigger the garbage compactor with: 1 GC.compact # available since Ruby 2.6 2 3 puts “Yay, a tidier heap sparks joy!" •
  48. Memory: Automatic Garbage Compaction • Most objects will be automatically

    transferred to the heap and compacted to improve memory usage and performance
  49. Concurrency / Parallelism • “It’s multi-core age today. Concurrency is

    very important. With Ractor, along with Async Fiber, Ruby will be a real concurrent language.” Matz
  50. Fiber Scheduler • It can be used to intercept blocking

    operations and yield.
  51. Fiber vs Thread • Fibers are more lightweight. They eat

    less memory. • The OS decides when to run/ pause your threads. • You have more control over when to run/pause your fibers.
  52. Fiber Scheduler 1 f = Fiber.new { puts 1; Fiber.yield;

    puts 2 } 2 3 f.resume 4 # 1 5 # => nil 6 7 f.resume 8 # 2 9 # => nil
  53. Fiber Scheduler 3 Thread.new do # in this thread, we'll

    have non-blocking fibers 4 5 Fiber.set_scheduler Scheduler.new 6 7 %w[2.6 2.7 3.0].each do |version| 8 Fiber.schedule do # Runs block of code in a separate Fiber 9 10 t = Time.now 11 # ... 16 Net::HTTP.get('rubyreferences.github.io', "/rubychanges/#{version}.html") 19 puts '%s: finished in %.3f' % [version, 20 Time.now - t] 21 end 22 end 23 end.join # At the END of the thread code, Scheduler will be called to dispatch 24 25 # all waiting fibers in a non-blocking manner 26 27 28 puts 'Total: finished in %.3f' % (Time.now - 29 start) 30 31 # Prints: 32 # 2.6: finished in 0.139 33 # 2.7: finished in 0.141 34 # 3.0: finished in 0.143 35 # Total: finished in 0.146
  54. Don’t Wait For Me! Scalable Concurrency for Ruby 3 by

    Samuel Williams
  55. Fiber Scheduler (Why?) • You don’t have to manually yield.

    • You end up with cleaner code in your fibers. • There is a scheduler implementation in the `async` gem. (source)
  56. Static Analysis • “Ruby seeks the future with static type

    checking, without type declaration, using abstract interpretation. RBS & TypeProf are the first step to the future. More steps to come.” Matz
  57. Static Analysis: RBS • RBS is a language to describe

    the structure of Ruby programs. • Ruby 3.0 ships with the `rbs` gem which includes the `rbs` CLI command.
  58. Static Analysis: RBS 1 # test.rb 2 class User 3

    def initialize(name:, age:) 4 @name, @age = name, age 5 end 6 7 attr_reader :name, :age 8 end 9 10 User.new(name: "John", age: 38)
  59. Static Analysis: RBS $ rbs prototype rb user.rb 1 #

    test.rb 2 class User 3 def initialize: (name: untyped name, age: 4 untyped age) -> untyped 5 6 attr_reader name: untyped 7 8 attr_reader age: untyped 9 end
  60. Static Analysis: RBS $ rbs help Usage: rbs [options...] [command...]

    Available commands: ast, list, ancestors, methods, method, validate, constant, paths, prototype, vendor, parse, test, version, help. Options: -r LIBRARY Load RBS files of the library -I DIR Load RBS files from the directory --no-stdlib Skip loading standard library signatures --repo DIR Add RBS repository --log-level LEVEL Specify log level (defaults to `warn`) --log-output OUTPUT Specify the file to output log (defaults to stderr)
  61. TypeProf • TypeProf is a type analysis tool bundled in

    the Ruby 3.0 package.
  62. TypeProf • It reads plain (non-type- annotated) Ruby code, analyzes

    what methods are defined and how they are used, and generates a prototype of type signature in RBS format.
  63. Static Analysis: TypeProf $ typeprof user.rb 1 # test.rb 2

    class User 3 def initialize: (name: String, age: 4 Integer) -> [String, Integer] 5 6 attr_reader name: String 7 8 attr_reader age: Integer 9 end
  64. Typeprof (Online Tool) https://mame.github.io/typeprof-playground/

  65. Typeprof (Demo)

  66. Static Analysis: Pragmatic View (Why?) • Exhaustiveness Checking (Sorbet) •

    Automatic Documentation of Public Method Signatures
  67. JIT Improvements • Significantly decreased the size of JIT-ed code

    (less memory-intensive) • Still not ready for optimizing workloads like Rails
  68. And more... • IRB Performance Improvement • Keyword arguments are

    separated from other arguments • Pattern matching (case/in) is no longer experimental • Arguments forwarding now supports leading arguments
  69. Positional and Keyword Arguments: ArgumentError 1 # This method accepts

    only a keyword argument 2 def foo(k: 1) 3 p k 4 end 5 6 h = { k: 42 } 7 8 # This method call passes a positional Hash argument 9 # In Ruby 2.7: The Hash is automatically converted to a keyword argument 10 # In Ruby 3.0: This call raises an ArgumentError 11 foo(h) 12 # => demo.rb:11: warning: Using the last argument as keyword parameters is 13 deprecated; maybe ** should be added to the call 14 # demo.rb:2: warning: The called method `foo' is defined here 15 # 42 16 17 # If you want to keep the behavior in Ruby 3.0, use double splat 18 foo(**h) #=> 42
  70. Positional and Keyword Arguments: ArgumentError • Best case scenario: Many

    ArgumentErrors all over the logs. • Worst case scenario: Weird, buggy behavior with Ruby 3.0.
  71. Upgrading to Ruby 3.0

  72. Notes • Upgrade to Ruby 2.7 first. You get slightly

    better performance than in 2.6. Also, you will see a bunch of deprecation warnings re: positional and keyword arguments. • Fix deprecation warnings re: positional arguments. • Proactively check that your dependencies work with Ruby 3.0. Maybe your first OSS contribution? • Backtraces in Ruby 3.0 behave like in Ruby 2.4: an error message and the line number where the exception occurs are printed first, and its callers are printed later
  73. Rails 6.1

  74. - Horizontal Sharding (DB) - Strict Loading - Delegated Types

    - Destroy Associations Async - Disallowed Deprecation Support - Performance Improvements - And more…
  75. Horizontal Sharding • Horizontal sharding is when you split up

    your database to reduce the number of rows on each database server, but maintain the same schema across "shards". This is commonly called “multi-tenant" sharding.
  76. Sharding (Config) 1 production: 2 primary: 3 database: my_primary_database 4

    adapter: mysql 5 primary_replica: 6 database: my_primary_database 7 adapter: mysql 8 replica: true 9 primary_shard_one: 10 database: my_primary_shard_one 11 adapter: mysql 12 primary_shard_one_replica: 13 database: my_primary_shard_one 14 adapter: mysql 15 replica: true
  77. ApplicationRecord 1 class ApplicationRecord < ActiveRecord::Base 2 self.abstract_class = true

    3 4 connects_to shards: { 5 default: { writing: :primary, reading: : 6 primary_replica }, 7 shard_one: { writing: :primary_shard_one, 8 reading: : 9 primary_shard_one_replica } 10 } 11 end
  78. Shard Use Case 1 ActiveRecord::Base.connected_to(role: :reading, 2 shard: :shard_one) do

    3 Record.first # lookup record from read replica of shard one 4 end 5
  79. Strict Loading • `config.active_record.strict_loading_by_default` is a boolean value that either

    enables or disables strict_loading mode by default. Defaults to `false`.
  80. Strict Loading 1 class Developer < ApplicationRecord 2 self.strict_loading_by_default =

    true 3 4 has_many :projects 5 end 6 7 dev = Developer.first 8 dev.projects.first 9 # raises ActiveRecord::StrictLoadingViolationError Exception: Developer is 10 # marked as strict_loading and Project cannot be lazily loaded. 11 12 dev = Developer.includes(:projects).first 13 dev.projects.first 14 # this works
  81. Strict Loading: Why? • It’s a strict way to make

    sure you don’t end up with N+1 queries.
  82. Delegated Types • “This is similar to what's called multi-table

    inheritance in Django, but instead of actual inheritance, this approach uses delegation to form the hierarchy and share responsibilities.” DHH • “It’s like ‘single-table inheritance’ but without inheritance (it uses delegation instead) and with multiple tables.” Ernesto
  83. Delegated Types 1 # Schema: entries[ id, account_id, creator_id, 2

    created_at, updated_at, entryable_type, 3 entryable_id ] 4 class Entry < ApplicationRecord 5 belongs_to :account 6 belongs_to :creator 7 delegated_type :entryable, types: %w[ Message Comment ] 8 9 end 10 11 # To be included in delegated classes 12 module Entryable 13 extend ActiveSupport::Concern 14 15 included do 16 has_one :entry, as: :entryable, touch: true 17 end 18 end
  84. Delegated Types 1 # Schema: messages[ id, subject ] 2

    class Message < ApplicationRecord 3 include Entryable 4 has_rich_text :content 5 end 6 7 # Schema: comments[ id, content ] 8 class Comment < ApplicationRecord 9 include Entryable 10 end
  85. Delegated Types: Why? • In case you need more flexibility

    and single- table inheritance doesn’t seem like the best way to go.
  86. Destroy Associations (Async) 1 class Account < ApplicationRecord 2 belongs_to

    :supplier, dependent: :destroy 3 4 end 5 6 # destroys associations synchronously
  87. Destroy Associations (Async) 1 class Account < ApplicationRecord 2 belongs_to

    :supplier, dependent: :destroy_async 3 4 end 5 6 # `:destroy_async` will enqueue a job to 7 destroy associated records in the background.
  88. Destroy Async: Why? • It can save you time (you

    don’t have to implement this in your application code) and it can quickly save you from request time outs after deleting a “god record”
  89. ActiveStorage • Permanent URLs for public storage blobs. Services can

    be configured in `config/ storage.yml` with a new key `public: true | false` to indicate whether a service holds public blobs or private blobs. Public services will always return a permanent URL. • You can now configure different services for different attachments. More granular control for services per attachment.
  90. ActiveStorage 1 # If you need the attachment to use

    a service 2 # which differs from the globally configured one, 3 # pass the +:service+ option. For instance: 4 class User < ActiveRecord::Base 5 has_one_attached :avatar, service: :s3 6 end
  91. ActiveStorage: Why? • It’s good to see some forward progress

    in feature development, especially increasing the flexibility of ActiveStorage.
  92. ActiveRecord: Performance Improvements • Avoid making queries where the value

    is an empty array. (https://github.com/rails/rails/ pull/37266) • Speed up queries when Rails knows that all values in the query are integers. (https:// github.com/rails/rails/pull/39009)
  93. Classic Autoloader Deprecated • New Rails projects discouraged from using

    the classic autoloader. • The default autoloader will be `zeitwerk` for all new Rails applications.
  94. Disallowed Deprecation Support • It allows the configuration of rules

    to match deprecation warnings that should not be allowed within the app.
  95. Disallowed Deprecation Support 1 ActiveSupport::Deprecation.disallowed_warnings = [ 2 "bad_method", 3

    :worse_method, 4 /(horrible|unsafe)_method/, 5 ] 6 7 # Or disallow everything: 8 9 ActiveSupport::Deprecation.disallowed_warnings = :all
  96. Disallowed Deprecation Support 1 if Rails.env.production? 2 ActiveSupport::Deprecation.disallowed_behavior = [:log]

    3 else 4 ActiveSupport::Deprecation.disallowed_behavior = [:raise] 5 end
  97. Disallowed Deprecation Support: Why? • When we eliminate deprecation warnings

    that appear in our app we want to be sure that the deprecations are never re-introduced. This will be very useful in future Ruby/Rails upgrade projects.
  98. Upgrading to Rails 6.1

  99. Notes • Upgrade to Rails 6.0 first • Address all

    deprecation warnings in the Rails 6.0 test and production logs • Proactively check that our dependencies work with Rails 6.1: Maybe your first OSS contribution? Check your Gemfile.lock for incompatibilities by using RailsBump • Focus on “Removals” in the Rails 6.1 release notes. Most notable: Remove deprecated ActiveRecord::Base#update_attributes and ActiveRecord::Base#update_attributes!
  100. Thank you! @etagwerker 100 Thank you!

  101. Resources 1. https://www.fastruby.io/blog/ruby/performance/how-fast-are-ractors.html 2. https://www.speedshop.co/2020/05/11/the-ruby-gvl-and-scaling.html 3. https://engineering.appfolio.com/appfolio-engineering/2019/9/13/benchmarking-fibers-threads-and-processes 4. https://docs.ruby-lang.org/en/3.0.0/doc/syntax/pattern_matching_rdoc.html 5.

    https://www.ruby-lang.org/en/news/2020/12/25/ruby-3-0-0-released/ 6. https://twitter.com/k0kubun/status/1256142302608650244 7. https://www.rubyguides.com/2019/11/what-are-fibers-in-ruby/ 8. https://www.rubyguides.com/2015/07/ruby-threads/ 9. https://www.mendelowski.com/docs/ruby/pattern-matching/ 10. https://twitter.com/keystonelemur/status/1349244719751196672/photo/1 11. https://www.toptal.com/ruby/ruby-pattern-matching-tutorial 12. https://medium.com/@k0kubun/ruby-3-0-jit-and-beyond-4d9404ce33c 13. https://developers.redhat.com/blog/2018/03/22/ruby-3x3-performance-goal/ 14. https://developers.redhat.com/blog/2018/03/22/ruby-3x3-performance-goal/ 15. https://blog.heroku.com/ruby-just-in-time-compilation 16. https://bugs.ruby-lang.org/issues/12589 17. https://scoutapm.com/blog/ruby-3-features 18. https://engineering.appfolio.com/appfolio-engineering/2019/3/22/ruby-27-and-the-compacting-garbage-collector 19. https://www.youtube.com/watch?v=Y29SSOS4UOc (Don’t wait for me! Scalable concurrency for Ruby 3!) 20. https://rubyreferences.github.io/rubychanges/3.0.html#non-blocking-fiber-and-scheduler 21. https://sorbet.org/docs/exhaustiveness 22. https://github.com/AaronC81/sord 23. https://weblog.rubyonrails.org/2020/12/9/Rails-6-1-0-release/ 24. https://www.fastruby.io/blog/rails/upgrades/upgrade-rails-from-5-2-to-6-0.html 25. https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ 26. https://github.com/socketry/async/pull/56 27. https://bugs.ruby-lang.org/issues/17100
  102. Thank you! @etagwerker 102 Questions?