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

Ernesto Tagwerker

January 26, 2021
Tweet

More Decks by Ernesto Tagwerker

Other Decks in Programming

Transcript

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

    Garbage Compaction - Fiber Scheduler - Static Analysis - JIT Improvements - And more…
  2. 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]
  3. 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"
  4. 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
  5. Endless Method Definitions 1 def square(x) = x * x

    2 3 puts "7 square = #{square(7)}"
  6. Ractor • Ractor is designed to provide a parallel execution

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

    has its own GVL, so you can have multiple threads within your ractor.
  8. 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
  9. 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
  10. 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)
  11. 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)
  12. <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
  13. <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
  14. <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
  15. 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
  16. 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
  17. 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
  18. 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'
  19. • 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:
  20. 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,
  21. 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!
  22. 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 •
  23. Ruby 3x3 • “The goal is to make Ruby 3

    run three times faster as compared to Ruby 2.0.” * Matz, 2016 (source)
  24. 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.
  25. 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
  26. 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
  27. 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!" •
  28. Memory: Automatic Garbage Compaction • Most objects will be automatically

    transferred to the heap and compacted to improve memory usage and performance
  29. 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
  30. 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.
  31. 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
  32. 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
  33. 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)
  34. 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
  35. 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.
  36. 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)
  37. 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
  38. 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)
  39. 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.
  40. 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
  41. Static Analysis: Pragmatic View (Why?) • Exhaustiveness Checking (Sorbet) •

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

    (less memory-intensive) • Still not ready for optimizing workloads like Rails
  43. 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
  44. 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
  45. Positional and Keyword Arguments: ArgumentError • Best case scenario: Many

    ArgumentErrors all over the logs. • Worst case scenario: Weird, buggy behavior with Ruby 3.0.
  46. 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
  47. - Horizontal Sharding (DB) - Strict Loading - Delegated Types

    - Destroy Associations Async - Disallowed Deprecation Support - Performance Improvements - And more…
  48. 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.
  49. 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
  50. 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
  51. 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
  52. 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`.
  53. 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
  54. Strict Loading: Why? • It’s a strict way to make

    sure you don’t end up with N+1 queries.
  55. 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
  56. 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
  57. 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
  58. Delegated Types: Why? • In case you need more flexibility

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

    :supplier, dependent: :destroy 3 4 end 5 6 # destroys associations synchronously
  60. 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.
  61. 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”
  62. 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.
  63. 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
  64. ActiveStorage: Why? • It’s good to see some forward progress

    in feature development, especially increasing the flexibility of ActiveStorage.
  65. 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)
  66. Classic Autoloader Deprecated • New Rails projects discouraged from using

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

    to match deprecation warnings that should not be allowed within the app.
  68. 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
  69. 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.
  70. 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!
  71. 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