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

RailsConf 2018 | The Future of Rails 6: Scalable by Default

RailsConf 2018 | The Future of Rails 6: Scalable by Default

We've all heard the phrase "Rails doesn't scale". Long running test suites and no standard for implementing multiple databases makes it hard scale monolithic Rails applications. Rails 6 will start making Rails scalable by default with parallel testing and improved support for using multiple databases. You'll no longer be forced to reinvent the wheel and create your own solution to these problems. In this talk we'll take a look why these improvements are important, how they work, and ways in which small ideas can quickly snowball into major changes. This is just the beginning of Rails 6.

Eileen M. Uchitelle

April 18, 2018
Tweet

More Decks by Eileen M. Uchitelle

Other Decks in Programming

Transcript

  1. The Future of Rails 6
    Scalable by Default

    View Slide

  2. Eileen M. Uchitelle
    eileencodes.com
    @eileencodes

    View Slide

  3. a

    View Slide

  4. Core Team

    View Slide

  5. “Rails doesn’t scale!”

    View Slide

  6. View Slide

  7. Rails doesn’t scale

    View Slide

  8. Rails doesn’t scale
    easily

    View Slide

  9. Rails doesn’t scale
    easily (yet)

    View Slide

  10. What does
    “scalable” mean?

    View Slide

  11. Your test suite shouldn’t
    block development

    View Slide

  12. Your application can
    handle data & traffic

    View Slide

  13. The Future of Rails 6
    Scalable by Default

    View Slide

  14. Parallel Testing
    Speeding up your test suite

    View Slide

  15. a

    View Slide

  16. a

    View Slide

  17. Forking
    - vs -
    Threads

    View Slide

  18. Forking Processes
    with dRB

    View Slide

  19. # test/test_helper.rb
    module ActiveSupport::TestCase
    parallelize(workers: 2)
    end

    View Slide

  20. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  21. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  22. # activesupport/lib/active_support/testing/parallelization
    module ActiveSupport::Testing::Parallelization
    […]
    def initialize(queue_size)
    @queue_size = queue_size
    @queue = Server.new
    @pool = []
    @url = DRb.start_service("drbunix:", @queue).uri
    end
    […]
    end

    View Slide

  23. # activesupport/lib/active_support/testing/parallelization
    module ActiveSupport::Testing::Parallelization
    […]
    def initialize(queue_size)
    @queue_size = queue_size
    @queue = Server.new
    @pool = []
    @url = DRb.start_service("drbunix:", @queue).uri
    end
    […]
    end

    View Slide

  24. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  25. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    DRb.stop_service

    after_fork(worker)

    queue = DRbObject.new_with_uri(@url)

    while job = queue.pop
    klass = job[0]
    method = job[1]
    […]

    View Slide

  26. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    DRb.stop_service

    after_fork(worker)

    queue = DRbObject.new_with_uri(@url)

    while job = queue.pop
    klass = job[0]
    method = job[1]
    […]

    View Slide

  27. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    DRb.stop_service

    after_fork(worker)

    queue = DRbObject.new_with_uri(@url)

    while job = queue.pop
    klass = job[0]
    method = job[1]
    […]

    View Slide

  28. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    DRb.stop_service

    after_fork(worker)

    queue = DRbObject.new_with_uri(@url)

    while job = queue.pop
    klass = job[0]
    method = job[1]
    […]

    View Slide

  29. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    […]

    while job = queue.pop
    klass = job[0]
    method = job[1]
    reporter = job[2]
    result = Minitest.run_one_method(klass, method)
    queue.record(reporter, result)
    end
    run_cleanup(worker)
    […]

    View Slide

  30. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    […]

    while job = queue.pop
    klass = job[0]
    method = job[1]
    reporter = job[2]
    result = Minitest.run_one_method(klass, method)
    queue.record(reporter, result)
    end
    run_cleanup(worker)
    […]

    View Slide

  31. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    […]

    while job = queue.pop
    klass = job[0]
    method = job[1]
    reporter = job[2]
    result = Minitest.run_one_method(klass, method)
    queue.record(reporter, result)
    end
    run_cleanup(worker)
    […]

    View Slide

  32. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    […]

    while job = queue.pop
    klass = job[0]
    method = job[1]
    reporter = job[2]
    result = Minitest.run_one_method(klass, method)
    queue.record(reporter, result)
    end
    run_cleanup(worker)
    […]

    View Slide

  33. module ActiveSupport::Testing::Parallelization
    […]
    def start
    […]
    end
    def shutdown
    @queue_size.times { @queue << nil }
    @pool.each { |pid| Process.waitpid(pid) }
    end
    […]
    end

    View Slide

  34. # test/test_helper.rb
    module ActiveSupport::TestCase
    parallelize(workers: 2)
    end

    View Slide


  35. test-database-0 test-database-1

    View Slide

  36. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    DRb.stop_service

    after_fork(worker)

    queue = DRbObject.new_with_uri(@url)

    while job = queue.pop
    klass = job[0]
    method = job[1]
    […]

    View Slide

  37. module ActiveSupport::Testing::Parallelization
    […]
    def start
    @pool = @queue_size.times.map do |worker|
    fork do
    […]

    while job = queue.pop
    klass = job[0]
    method = job[1]
    reporter = job[2]
    result = Minitest.run_one_method(klass, method)
    queue.record(reporter, result)
    end
    run_cleanup(worker)
    […]

    View Slide

  38. # test/test_helper.rb
    module ActiveSupport::TestCase
    parallelize(workers: 2)
    parallelize_setup do |worker|
    ActiveRecord::Tasks::DatabaseTasks.create(:animals)
    ActiveRecord::Base.establish_connection(:animals)
    ActiveRecord::Tasks::DatabaseTasks.load_schema
    end
    parallelize_teardown do |worker|
    ActiveRecord::Tasks::DatabaseTasks.drop(:animals)
    end
    end

    View Slide

  39. Threaded testing
    with Minitest

    View Slide

  40. # test/test_helper.rb
    module ActiveSupport::TestCase
    parallelize(workers: 2, with: :threads)
    end

    View Slide

  41. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  42. View Slide


  43. Connection ID:
    #03938752
    Connection ID:
    #83321340

    View Slide


  44. Connection ID:
    #83321340
    Connection ID:
    #83321340

    View Slide

  45. This is so weird. We
    seem to have an
    isolation problem…

    View Slide

  46. View Slide


  47. Rails
    opens a
    connection

    View Slide


  48. Puma opens
    a second
    connection
    Rails
    opens a
    connection

    View Slide


  49. Puma opens
    a second
    connection
    Rails
    opens a
    connection

    View Slide

  50. Test thread
    connection
    number 1
    Test thread
    connection
    number 2

    View Slide

  51. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  52. $ rails test

    View Slide

  53. $ PARALLEL_WORKERS=14 rails test

    View Slide

  54. def parallelize(workers: 2, with: :processes)
    workers = ENV["PARALLEL_WORKERS"].to_i if ENV["PARALLEL_WORKERS"]
    return if workers <= 1
    executor = case with
    when :processes
    Testing::Parallelization.new(workers)
    when :threads
    Minitest::Parallel::Executor.new(workers)
    else
    raise ArgumentError, "#{with} is 

    not a supported parallelization executor."
    end
    self.lock_threads = false if defined?(self.lock_threads) && 

    with == :threads
    Minitest.parallel_executor = executor
    parallelize_me!
    end

    View Slide

  55. Parallel Testing PR
    github.com/rails/rails/pull/31900

    View Slide

  56. Your test suite shouldn’t
    block development

    View Slide

  57. Your application can
    handle data & traffic

    View Slide

  58. View Slide

  59. View Slide








  60. DB
    1
    DB
    2
    DB
    3

    View Slide

  61. Multiple Databases
    Making multi-db behavior easy

    View Slide








  62. Primary

    View Slide








  63. DB
    1
    DB
    2
    DB
    3

    View Slide

  64. Table
    id: integer
    name: string
    Table
    id: integer
    name: string
    Table
    id: integer
    name: string
    Table
    id: integer
    name: string
    Primary Animals

    View Slide

  65. X Best practices are undocumented
    The State of Multi-db in Rails 5

    View Slide

  66. # database.yml
    production:
    <<: *default
    database: db_production
    production_animals:
    <<: *default
    database: db_animals_production

    View Slide

  67. # database.yml
    production:
    <<: *default
    database: db_production
    production_animals:
    <<: *default
    database: db_animals_production

    View Slide

  68. # 3-tier database.yml
    production:
    primary:
    <<: *default
    database: db_production
    animals:
    <<: *default
    database: db_animals_production

    View Slide

  69. db/animals_migrate
    ↳ 20180206210429_create_dogs.rb
    ↳ 20180206210437_create_cats.rb

    View Slide

  70. ActiveRecord::Migrator.migrations_paths =
    "db/animals_migrate"
    ActiveRecord::Base.migrate

    View Slide

  71. X Best practices are undocumented
    X Migrations don’t work
    The State of Multi-db in Rails 5

    View Slide

  72. # app/models/animals_base.rb
    class AnimalsBase < ActiveRecord::Base
    self.abstract_class = true
    establish_connection :animals
    end

    View Slide

  73. # app/models/dog.rb
    class Dog < AnimalsBase
    […]
    end
    # app/models/cat.rb
    class Cat < AnimalsBase
    […]
    end

    View Slide

  74. X Best practices are undocumented
    X Migrations don’t work
    X Database tasks don’t exist
    The State of Multi-db in Rails 5

    View Slide

  75. ActiveRecord::AdapterNotSpecified:
    'production' database is not configured.

    View Slide

  76. # activerecord/lib/active_record/railtie.rb
    initializer "active_record.initialize_database" do
    ActiveSupport.on_load(:active_record) do
    self.configurations =
    Rails.application.config.database_configuration
    begin
    establish_connection
    rescue ActiveRecord::NoDatabaseError
    warn <<-end_warning

    View Slide

  77. # activerecord/lib/active_record/railtie.rb
    initializer "active_record.initialize_database" do
    ActiveSupport.on_load(:active_record) do
    self.configurations =
    Rails.application.config.database_configuration
    begin
    establish_connection
    rescue ActiveRecord::NoDatabaseError
    warn <<-end_warning

    View Slide

  78. X Best practices are undocumented
    X Migrations don’t work
    X Database tasks don’t exist
    X Default connection was broken
    The State of Multi-db in Rails 5

    View Slide

  79. Migrations
    Connections
    don’t work
    Rake tasks

    View Slide

  80. View Slide

  81. Fixing migrations for
    multiple databases

    View Slide

  82. path = Rails.root.to_s + "/db/animals_migrate"
    desc "Migrate the cluster db"
    task migrate: :environment do
    ActiveRecord::Migrator.migrations_paths = path
    ActiveRecord::Base.establish_connection(:animals)
    ActiveRecord::Tasks::DatabaseTasks.migrate
    db_namespace["db:schema:dump"].invoke
    end

    View Slide

  83. # 3-tier database.yml
    production:
    primary:
    <<: *default
    database: db_production
    animals:
    <<: *default
    database: db_animals_production
    migrations_paths: db/animals_migrate

    View Slide

  84. >> Dog.connection.migrations_paths
    => "db/animals_migrate"

    View Slide

  85. Migrations Paths Refactoring PR
    github.com/rails/rails/pull/31727

    View Slide

  86. X Best practices are undocumented
    ✔ Migrations don’t work
    X Database tasks don’t exist
    X Default connection was broken
    The State of Multi-db in Rails 5

    View Slide

  87. Migrations
    Connections
    don’t work
    Rake tasks

    View Slide

  88. Improving the database
    tasks for multi-dbs

    View Slide

  89. rails db:drop
    rails db:create
    rails db:migrate

    View Slide

  90. >> ActiveRecord::Base.configurations["production"]
    >> {
    "adapter"=>"mysql2",
    "database"=>"single_database_production"
    }

    View Slide

  91. >> ActiveRecord::Base.configurations["production"]
    >> {
    "primary"=>{"adapter"=>"mysql2",
    "database"=>"multiple_databases_production"},
    "animals"=>{"adapter"=>"mysql2",
    “database"=>"multiple_databases_production"}
    }

    View Slide

  92. module ActiveRecord
    module DatabaseConfigurations
    class DatabaseConfig
    attr_reader :env_name, :spec_name, :config
    def initialize(env_name, spec_name, config)
    @env_name = env_name
    @spec_name = spec_name
    @config = config
    end
    end
    end
    end

    View Slide

  93. module ActiveRecord
    module DatabaseConfigurations
    class DatabaseConfig
    attr_reader :env_name, :spec_name, :config
    def initialize(env_name, spec_name, config)
    @env_name = env_name
    @spec_name = spec_name
    @config = config
    end
    end
    end
    end

    View Slide

  94. module ActiveRecord
    module DatabaseConfigurations
    class DatabaseConfig
    attr_reader :env_name, :spec_name, :config
    def initialize(env_name, spec_name, config)
    @env_name = env_name
    @spec_name = spec_name
    @config = config
    end
    end
    end
    end

    View Slide

  95. module ActiveRecord
    module DatabaseConfigurations
    class DatabaseConfig
    attr_reader :env_name, :spec_name, :config
    def initialize(env_name, spec_name, config)
    @env_name = env_name
    @spec_name = spec_name
    @config = config
    end
    end
    end
    end

    View Slide

  96. >> db_config = DatabaseConfig.new("production",
    "animals", {"adapter"=>"mysql2",
    "database"=>"multiple_databases_production_animals"
    })
    >>
    #fig:0x00007f994571edb0 @env_name="production",
    @spec_name="animals", @config={"adapter"=>"mysql2",
    "database"=>"multiple_databases_production_animals"
    }>

    View Slide

  97. >> db_config.env_name
    "production"

    View Slide

  98. >> db_config.env_name
    "production"
    >> db_config.spec_name
    "animals"

    View Slide

  99. >> db_config.env_name
    "production"
    >> db_config.spec_name
    "animals"
    >> db_config.config
    {…"database"=>"multiple_databases_producti
    on_animals"}

    View Slide

  100. def self.db_configs(configs = configurations)
    configs.each_pair.flat_map do |env_name, config|
    walk_configs(env_name, "primary", config)
    end
    end

    View Slide

  101. def self.configs_for(env, configs, &blk)
    env_with_configs =
    db_configs(configs).select do |db_config|
    db_config.env_name == env
    end
    if block_given?
    env_with_configs.each do |ewc|
    yield ewc.spec_name, ewc.config
    end
    else
    env_with_configs
    end
    end

    View Slide

  102. def self.configs_for(env, configs, &blk)
    env_with_configs =
    db_configs(configs).select do |db_config|
    db_config.env_name == env
    end
    if block_given?
    env_with_configs.each do |ewc|
    yield ewc.spec_name, ewc.config
    end
    else
    env_with_configs
    end
    end

    View Slide

  103. def self.configs_for(env, configs, &blk)
    env_with_configs =
    db_configs(configs).select do |db_config|
    db_config.env_name == env
    end
    if block_given?
    env_with_configs.each do |ewc|
    yield ewc.spec_name, ewc.config
    end
    else
    env_with_configs
    end
    end

    View Slide

  104. >> DatabaseConfigurations.configs_for("production")
    >>[#Config:0x00007f994571edb0 @env_name="production",
    @spec_name="primary", @config={"adapter"=>"mysql2",
    "database"=>"multiple_databases_production"}>,
    #fig:0x00007f994571edb0 @env_name="production",
    @spec_name="animals", @config={"adapter"=>"mysql2",
    "database"=>"multiple_databases_production_animals"
    }>]

    View Slide

  105. task migrate: :load_config do
    ActiveRecord::Tasks::DatabaseTasks.migrate
    db_namespace["_dump"].invoke
    end

    View Slide

  106. task migrate: :load_config do
    ActiveRecord::DatabaseConfigurations.configs_for(
    Rails.env
    ) do |_, config|
    ActiveRecord::Base.establish_connection(config)
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
    end

    View Slide

  107. task migrate: :load_config do
    ActiveRecord::DatabaseConfigurations.configs_for(
    Rails.env
    ) do |_, config|
    ActiveRecord::Base.establish_connection(config)
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
    end

    View Slide

  108. task migrate: :load_config do
    ActiveRecord::DatabaseConfigurations.configs_for(
    Rails.env
    ) do |_, config|
    ActiveRecord::Base.establish_connection(config)
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
    end

    View Slide

  109. task migrate: :load_config do
    ActiveRecord::DatabaseConfigurations.configs_for(
    Rails.env
    ) do |_, config|
    ActiveRecord::Base.establish_connection(config)
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    db_namespace["_dump"].invoke
    end

    View Slide

  110. rails db:drop
    rails db:create
    rails db:migrate

    View Slide

  111. rails db:drop:primary
    rails db:drop:animals
    rails db:create:primary
    rails db:create:animals
    rails db:migrate:primary
    rails db:migrate:animals

    View Slide

  112. namespace :migrate
    ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
    desc "Migrate #{spec_name} database"
    task spec_name => :load_config do
    db_config = ActiveRecord::DatabaseConfigurations
    .config_for_env_and_spec(Rails.env, spec_name)
    ActiveRecord::Base.establish_connection(
    db_config.config
    )
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    end
    end
    end

    View Slide

  113. namespace :migrate
    ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
    desc "Migrate #{spec_name} database"
    task spec_name => :load_config do
    db_config = ActiveRecord::DatabaseConfigurations
    .config_for_env_and_spec(Rails.env, spec_name)
    ActiveRecord::Base.establish_connection(
    db_config.config
    )
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    end
    end
    end

    View Slide

  114. namespace :migrate
    ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
    desc "Migrate #{spec_name} database"
    task spec_name => :load_config do
    db_config = ActiveRecord::DatabaseConfigurations
    .config_for_env_and_spec(Rails.env, spec_name)
    ActiveRecord::Base.establish_connection(
    db_config.config
    )
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    end
    end
    end

    View Slide

  115. namespace :migrate
    ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
    desc "Migrate #{spec_name} database"
    task spec_name => :load_config do
    db_config = ActiveRecord::DatabaseConfigurations
    .config_for_env_and_spec(Rails.env, spec_name)
    ActiveRecord::Base.establish_connection(
    db_config.config
    )
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    end
    end
    end

    View Slide

  116. namespace :migrate
    ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
    desc "Migrate #{spec_name} database"
    task spec_name => :load_config do
    db_config = ActiveRecord::DatabaseConfigurations
    .config_for_env_and_spec(Rails.env, spec_name)
    ActiveRecord::Base.establish_connection(
    db_config.config
    )
    ActiveRecord::Tasks::DatabaseTasks.migrate
    end
    end
    end
    end

    View Slide

  117. rails db:drop:primary
    rails db:drop:animals
    rails db:create:primary
    rails db:create:animals
    rails db:migrate:primary
    rails db:migrate:animals

    View Slide

  118. Database task improvements
    github.com/rails/rails/pull/32274
    github.com/rails/rails/compare/
    ab43b5405ea2...5ddcda6d5f56

    View Slide

  119. X Best practices are undocumented
    ✔ Migrations don’t work
    ✔ Database tasks don’t exist
    X Default connection was broken
    The State of Multi-db in Rails 5

    View Slide

  120. Connections
    don’t work
    Migrations
    Rake tasks

    View Slide

  121. View Slide

  122. Thanks
    @tenderlove!

    View Slide

  123. “Are you concerned
    that Rails will become
    bloated?”

    View Slide

  124. What would Rails look like
    if we’d upstreamed these
    features 5 years ago?

    View Slide

  125. a +

    View Slide

  126. ' (
    )
    *
    +
    ,

    View Slide

  127. View Slide

  128. Maybe Rails doesn’t
    scale…?

    View Slide

  129. Your application
    isn’t special.

    View Slide

  130. Forces you to write
    generic code

    View Slide

  131. Train your future
    workforce

    View Slide

  132. Give back to the
    community

    View Slide

  133. '
    (
    )
    *
    +
    ,

    View Slide

  134. a♥

    View Slide

  135. Scaling should
    make us happy

    View Slide

  136. Rails isn’t dying,
    it’s maturing

    View Slide

  137. View Slide

  138. View Slide

  139. View Slide

  140. “Rails does scale!”

    View Slide

  141. The Future of Rails 6
    Scalable by Default

    View Slide

  142. Eileen M. Uchitelle
    eileencodes.com
    @eileencodes

    View Slide