Rails7.2で出る新機能を紹介 & どう実装されているかみてみる

## はじめに

Rails7.2の新機能について気になったので調べてみました。


Rails 7.2の新機能は、[Ruby on Rails Guides](https://edgeguides.rubyonrails.org/7_2_release_notes.html)を参考にしていきす。
現時点だと、7.2.0.beta2までしか出ていないですが、この時点までのでまずは書いてきます。


## rails new myapp --devcontainer
rails newに `--devcontainer` オプションが追加されました。

devcontainerを用意できるようになりました。

コードで言うと[この辺り](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/app_base.rb#L114-L115
)です。

```ruby
        class_option :devcontainer,        type: :boolean, default: false,
                                           desc: "Generate devcontainer files"
```

この`devcontainer`オプションが`true`になったとき動くのが[この辺り](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/app_generator.rb#L271-L283)です。

`devcontainer_options`を定義して、`DevcontainerGenerator`クラスに渡し、 `invoke_all`で`DevcontainerGenerator`のPublicメソッドを実行させていますね。

```ruby
    def devcontainer
      devcontainer_options = {
        database: options[:database],
        redis: !(options[:skip_action_cable] && options[:skip_active_job]),
        system_test: depends_on_system_test?,
        active_storage: !options[:skip_active_storage],
        dev: options[:dev],
        node: using_node?,
        app_name: app_name
      }


      Rails::Generators::DevcontainerGenerator.new([], devcontainer_options).invoke_all
    end
```

`DevcontainerGenerator` は[こんな感じ](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/devcontainer/devcontainer_generator.rb#L7-L51)になっています。

```ruby
    class DevcontainerGenerator < Base # :nodoc:
    # 省略
    # ~~
      def create_devcontainer
        empty_directory ".devcontainer"


        template "devcontainer/devcontainer.json", ".devcontainer/devcontainer.json"
        template "devcontainer/Dockerfile", ".devcontainer/Dockerfile"
        template "devcontainer/compose.yaml", ".devcontainer/compose.yaml"
      end
    # ~~
    # 省略
```

以下の3つのファイルを作成しています。

1. `.devcontainer/devcontainer.json`
1. `.devcontainer/Dockerfile`
1. `.devcontainer/compose.yaml` 

上記のファイルの元となっているテンプレートは[ここ](https://github.com/rails/rails/tree/7-2-stable/railties/lib/rails/generators/rails/devcontainer/templates/devcontainer)です。


## allow_browser

Railsの各Actionにアクセスできるブラウザを制限する機能です。
使い方はこんな感じ。
```ruby
class ApplicationController < ActionController::Base
  # allow_browser versions: :modern
  allow_browser versions: { safari: 16.4, firefox: 121, ie: false }, only: :show
end
```


`AllowBrowser` Moduleが追加されていて、`ActionController`でModuleを読み込んでいます。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/actionpack/lib/action_controller.rb#L32)

```ruby
module ActionController
  extend ActiveSupport::Autoload
    # ~~ 省略
    autoload_under "metal" do
    　　autoload :AllowBrowser
      # ~~省略
    end
  end
end
```

`AllowBrowser module`に `allow_browser`メソッドが定義されています。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/actionpack/lib/action_controller/metal/allow_browser.rb#L47-L49)

`before_action`で`allow_browser`メソッドを実行しているようです。

```ruby
      def allow_browser(versions:, block: -> { render file: Rails.root.join("public/406-unsupported-browser.html"), layout: false, status: :not_acceptable }, **options)
        before_action -> { allow_browser(versions: versions, block: block) }, **options
      end
```

`AllowBrowser#allow_browser`メソッド内で実行されている`allow_browser`は以下のように定義されています。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/actionpack/lib/action_controller/metal/allow_browser.rb#L53-L61)

`BrowserBlocker`クラスで`blocked?`で判定していますね。
```ruby
      def allow_browser(versions:, block:)
        require "useragent"


        if BrowserBlocker.new(request, versions: versions).blocked?
          ActiveSupport::Notifications.instrument("browser_block.action_controller", request: request, versions: versions) do
            instance_exec(&block)
          end
        end
      end
```

`blocked?`の判定の仕方は[こんな感じ](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/actionpack/lib/action_controller/metal/allow_browser.rb#L74-L76)です。
「`UserAgent`が存在している」 かつ 「サポートされていないブラウザである」 場合は `blocked?`が`true`になるようです。
```ruby
        def blocked?
          user_agent_version_reported? && unsupported_browser?
        end
```


## Ruby3.1以上の必要がある

以下のような感じで3.1.0以上である必要があるように定義されていました。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/tools/rail_inspector/rail_inspector.gemspec#L14)

```ruby
  spec.required_ruby_version = ">= 3.1.0"
```

## デフォルトでpwa用のファイルが生成されるようになった

pwa_controllerが定義されています。[こちら](https://github.com/rails/rails/blob/7-2-stable/railties/lib/rails/pwa_controller.rb)

```ruby
class Rails::PwaController < Rails::ApplicationController # :nodoc:
  skip_forgery_protection

  def service_worker
    render template: "pwa/service-worker", layout: false
  end

  def manifest
    render template: "pwa/manifest", layout: false
  end
end
```

pwa/service-worker,pwa/manifestファイルが生成されます。
テンプレートは[こちら](https://github.com/rails/rails/tree/7-2-stable/railties/lib/rails/generators/rails/app/templates/app/views/pwa)にあります。


## デフォルトでrubocop-rails-omakaseが用意されます

gemはこちら

https://github.com/rails/rubocop-rails-omakase

`rails new . --skip_rubocop` でinstallをskipすることもできます。

`rails new --devcontainer` のときと同様に以下のようにオプションが定義されています。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/app_base.rb#L102-L103)

```ruby
        class_option :skip_rubocop,        type: :boolean, default: nil,
                                           desc: "Skip RuboCop setup"
```

このオプションが以下の3箇所でrubocopは関係しています。

1つは.rubocop.ymlの生成 [こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/app_generator.rb#L92-L94)

```ruby
    def rubocop
      template "rubocop.yml", ".rubocop.yml"
    end
```

もう1つは`bin/rubocop`の生成 [こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/app_generator.rb#L111-L117)

```ruby
    def bin
      exclude_pattern = Regexp.union([(/rubocop/ if skip_rubocop?), (/brakeman/ if skip_brakeman?)].compact)
      directory "bin", { exclude_pattern: exclude_pattern } do |content|
        "#{shebang}\n" + content
      end
      chmod "bin", 0755 & ~File.umask, verbose: false
    end
```

もう1つは、CI上でrubocopのjobの生成もあります。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt#L45-L59)

```yml
<%- unless skip_rubocop? -%>
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version
          bundler-cache: true

      - name: Lint code for consistent style
        run: bin/rubocop -f github
```

## デフォルトでGitHub ActionsのCIが用意されます。

`rails new . --skip_ci` でinstallをskipすることもできます。


`rails new . --skip_rubocop` のときと同様に以下のようにオプションが定義されています。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/app_base.rb#L108-L109)

```ruby
        class_option :skip_ci,             type: :boolean, default: nil,
                                           desc: "Skip GitHub CI files"
```

このオプションがfalse、つまりciを生成するときは[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/app_generator.rb#L86-L90)のように動きます。

1. `.github/workflows` フォルダ作成
1. `.github/workflows/ci.yml` ファイル作成
1. `.github/dependabot.yml` ファイル作成

```ruby
    def cifiles
      empty_directory ".github/workflows"
      template "github/ci.yml", ".github/workflows/ci.yml"
      template "github/dependabot.yml", ".github/dependabot.yml"
    end
```

## デフォルトでbrakemanが用意されます

`rails new . --skip_brakeman` でinstallをskipすることもできます。

`rails new --skip_rubocop` のときと同様に以下のようにオプションが定義されています。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/app_base.rb#L105-L106)

```ruby
        class_option :skip_brakeman,       type: :boolean, default: nil,
                                           desc: "Skip brakeman setup"
```

このオプションによって、`bin/brakeman`コマンドの生成がされます。

```ruby
    def bin
      exclude_pattern = Regexp.union([(/rubocop/ if skip_rubocop?), (/brakeman/ if skip_brakeman?)].compact)
      directory "bin", { exclude_pattern: exclude_pattern } do |content|
        "#{shebang}\n" + content
      end
      chmod "bin", 0755 & ~File.umask, verbose: false
    end
```

またCIでbrakemanのJobの生成もされます。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt#L27-L42)

```yml
<%- if options[:javascript] == "importmap" -%>
  scan_js:
    runs-on: ubuntu-latest


    steps:
      - name: Checkout code
        uses: actions/checkout@v4


      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version
          bundler-cache: true


      - name: Scan for security vulnerabilities in JavaScript dependencies
        run: bin/importmap audit
```

## デフォルトのpumaのスレッド数が5から3に変更

この変更は、デフォルトのスレッド数がいくつが相応しいのかという[このissue](https://github.com/rails/rails/issues/50450)で話題になっていました。

結局`3つ`になったようです。

`puma.rb`のテンプレートにある`threads_count`が3になっていました。[こちら](https://github.com/rails/rails/blob/de53ffd6a121b6772afc5be4eafeb4cdb333b4e3/railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt#L23)

```ruby
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
```


## enqueue_after_transaction_commit オプション
トランザクションの最中にエンキューするか、Commit後にエンキューするかを設定できるオプションです。

```ruby
Topic.transaction do
  topic = Topic.create

  NewTopicNotificationJob.perform_later(topic)
end
```

上記のコードのようなコードを書いたとき、今までは`Transaction`がCommitされる前にJobがキューに登録され、Jobが動く可能性がありました。

`enqueue_after_transaction_commit`オプションを使用すると、`Transaction`内にJobをキューするコードを書いても、commit後にキューさせることができます。


このオプションがある場所は[こちら](https://github.com/rails/rails/blob/85630de889b61f8e12da167cd91f7fb07c0fc350/activejob/lib/active_job/enqueue_after_transaction_commit.rb#L14-L18)

```ruby
      # It can be set on a per job basis:
      #  - `:always` forces the job to be deferred.
      #  - `:never` forces the job to be queued immediately.
      #  - `:default` lets the queue adapter define the behavior (recommended).
      class_attribute :enqueue_after_transaction_commit, instance_accessor: false, instance_predicate: false, default: :never
```

- always
- never
- default

が選択できます。

この選択によって、以下のように、`after_transaction`が`true`/`false`になり、`Transaction`のCommit後にエンキューするかどうかに影響します。[こちら](https://github.com/rails/rails/blob/85630de889b61f8e12da167cd91f7fb07c0fc350/activejob/lib/active_job/enqueue_after_transaction_commit.rb#L22-L42)


```ruby
      def raw_enqueue
        after_transaction = case self.class.enqueue_after_transaction_commit
        when :always
          true
        when :never
          false
        else # :default
          queue_adapter.enqueue_after_transaction_commit?
        end

        if after_transaction
          self.successfully_enqueued = true
          ActiveRecord.after_all_transactions_commit do
            self.successfully_enqueued = false
            super
          end
          self
        else
          super
        end
      end
```


## Ruby3.3+の場合デフォルトでYJITが有効化

`Configuration`クラスの`initialize`で`yjit`が追加されています。[こちら](
https://github.com/rails/rails/blob/7c61bbd31c39254e8e7cbdaca34e5b8b5828eec1/railties/lib/rails/application/configuration.rb#L84)

```ruby
  def initialize(*)
    super
    # 省略
    @yjit                                    = false
    # 省略
  end

  def load_defaults(target_version)
    # 省略
    case target_version.to_s
    when "7.2"
      load_defaults "7.1"

      self.yjit = true

      if respond_to?(:active_job)
        active_job.enqueue_after_transaction_commit = :default
      end
    # 省略
  end
```

`load_defaults`内でRailsのバージョンが7.2だったら、`yjit`をtrueに変更しているのが分かります。


##  デフォルトのDockerfileにjemallocを設定してメモリ割り当てを最適化

jamellocについては[こちら](https://tech.medpeer.co.jp/entry/ruby-jemalloc#:~:text=jemalloc%20%E3%81%A8%E3%81%AF%20Meta%EF%BC%88%E6%97%A7,%E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E3%82%92%E8%AC%B3%E3%81%A3%E3%81%A6%E3%81%84%E3%81%BE%E3%81%99%E3%80%82)が分かりやすいかもしれません。

コードだとこの辺りです。[こちら](https://github.com/rails/rails/blob/7c61bbd31c39254e8e7cbdaca34e5b8b5828eec1/railties/lib/rails/generators/app_base.rb#L574C27-L588)

```ruby
      def dockerfile_base_packages
        # Add curl to work with the default healthcheck strategy in Kamal
        packages = ["curl"]

        # ActiveRecord databases
        packages << database.base_package unless skip_active_record?

        # ActiveStorage preview support
        packages << "libvips" unless skip_active_storage?

        # jemalloc for memory optimization
        packages << "libjemalloc2"

        packages.compact.sort
      end
```

`packages`という配列の変数に、`libjemalloc2`を入れています。

それを以下のDockerfileのテンプレートで展開して、`apt install`されるようです。[こちら](https://github.com/rails/rails/blob/7c61bbd31c39254e8e7cbdaca34e5b8b5828eec1/railties/lib/rails/generators/rails/app/templates/Dockerfile.tt#L17-L19)

```Dockerfile
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y <%= dockerfile_base_packages.join(" ") %> && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives
```

## bin/setupにpuma-devの設定の提案を追加

[puma-dev](https://github.com/puma/puma-dev)はDockerを使わない場合の、開発環境のRailsアプリの開発用サーバーの設定を楽にするものです。

その提案が`bin/setup`にコメントで追加されています。[こちら](https://github.com/rails/rails/blob/7c61bbd31c39254e8e7cbdaca34e5b8b5828eec1/railties/lib/rails/generators/rails/app/templates/bin/setup.tt#L44-L46)

```sh
  # puts "\n== Configuring puma-dev =="
  # system "ln -nfs #{APP_ROOT} ~/.puma-dev/#{APP_NAME}"
  # system "curl -Is https://#{APP_NAME}.test/up | head -n 1"
```



## おわりに
以上で、Rails Guideに載っている7.2の新機能については、ざっと紹介できたと思います。
Railsリポジトリの中身のコードをたくさん見れて楽しい機会でした。
今後、追加されたりしたら、追記していきます！