- Ruby / Rails関連
週刊Railsウォッチ: Rails 8でSprocketsがPropshaftに置き換わる、devcontainerサポートほか(20240228)
こんにちは、hachi8833です。
🔗Rails: 先週の改修(Rails公式ニュースより)
🔗 rails new
で.devcontainer用ファイルをセットアップするようになった
新規アプリ作成時に.devcontainerフォルダとそのコンテンツを生成するようになった。
この.devcontainerフォルダには、リモートコンテナ内でアプリを起動して開発するのに必要なものがすべて含まれる。
コンテナのセットアップには以下が含まれる。
- redisコンテナ(KredisやAction Cableなど向け)
- データベース(SQLite、PostgreSQL、MySQL、MariaDBのいずれか)
- ヘッドレスChromeコンテナ(システムテスト用)
- Active Storage(ローカルディスク利用設定済み、プレビュー機能も動く)
アプリのセットアップでこれらのオプションのいずれかをスキップすると、コンテナのコンフィグに含まれなくなる。
--skip-devcontainer
オプションを指定することでファイルをスキップできる。Andrew Novoselac & Rafael Mendonça França
システムテストのアプリケーションサーバー設定用の
SystemTestCase#served_by
を導入デフォルトではlocalhostになる。このメソッドを使うことで
host
やport
を手動設定可能になる。class ApplicationSystemTestCase < ActionDispatch::SystemTestCase served_by host: "testserver", port: 45678 end
Andrew Novoselac & Rafael Mendonça França
同Changelogより
つっつきボイス:「このプルリクを見るまでdevcontainerを知りませんでした」「devcontainerはVSCodeでDockerコンテナ内での開発を行うときのセットアップを行うものとして少し前に登場していましたね: VSCode以外にもJetBrains IDEでも使えますし、同じマイクロソフトがやっていることもあってGitHub Codespacesでも公式にサポートされていますね」「なるほど」「devcontainerはフロントエンド系でよく使われている印象があるかな: VSCodeでdevcontainerの開発用コンテナを立ち上げるとリモートから接続できたりと、VSCodeとのインテグレーションが進んでいるようです」
参考: Devcontainer ってなに? 実際につかってみる
参考: 開発コンテナーの概要 - GitHub Docs
参考: Dev Containers | IntelliJ IDEA ドキュメント
「rails new
のオプションに応じて.devcontainerフォルダの下にcompose.yamlとdevcontainer.jsonを生成するんですね」「システムテスト用のヘッドレスChromeコンテナとかRedisとか、必要そうなものが入っている感じ: 既存プロジェクトでdevcontainerをセットアップするときのひな形にも使えそう👍」「自分でゼロから作らなくても公式のセットアップが使えるのはいいですね」
# .devcontainer/devcontainer.json
// For format details, see https://aka.ms/devcontainer.json.
{
"name": "Rails project development",
"dockerComposeFile": "compose.yaml",
"service": "rails",
"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
// Features to add to the dev container. More info: https://containers.dev/features.
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {
"version": "latest"
}
},
"containerEnv": {
"PGHOST": "postgres",
"PGUSER": "postgres",
"PGPASSWORD": "postgres",
"MYSQL_HOST": "mariadb",
"REDIS_URL": "redis://redis/0",
"MEMCACHE_SERVERS": "memcached:11211"
},
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// This can be used to network with other containers or the host.
// "forwardPorts": [3000, 5432],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": ".devcontainer/boot.sh",
// Configure tool-specific properties.
"customizations": {
"vscode": {
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"Shopify.ruby-lsp"
]
}
},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
「ところでcompose.yamlを見ていたらUnixのsleep
コマンドにinfinity
という指定があるのを見つけた↓」「こんな指定があるんですね」
# https://github.com/rails/rails/blob/main/railties/test/fixtures/.devcontainer/compose.yaml
services:
rails-app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
volumes:
- ../..:/workspaces:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
networks:
- default
# Uncomment the next line to use a non-root user for all processes.
# user: vscode
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
ports:
- 45678:45678
depends_on:
- selenium
- redis
selenium:
image: seleniarm/standalone-chromium
restart: unless-stopped
networks:
- default
redis:
image: redis:7.2
restart: unless-stopped
networks:
- default
volumes:
- redis-data:/data
volumes:
redis-data:
参考: sleep infinity で無限に待つ - tmtms のメモ
「あとこのプルリクはChangelogエントリがもう1つあって、#SystemTestCase#served_by
が追加されたそうです↓」「なるほど、システムテストでホスト名やポートを指定するときに使えるんですね: たしかに環境によってはホスト名やポートを外から注入したいことはよくある」
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
served_by host: "testserver", port: 45678
end
🔗 ActiveRecord::Base.with_connection
を追加
コネクションを短い時間だけリースするショートカットとして
ActiveRecord::Base.with_connection
を追加する。リースされたコネクションがyieldされると、ブロックの実行中は
ActiveRecord::Base.connection
で同じコネクションがyieldされる。これは、リクエストやジョブの継続中コネクションをリースし続けることなく、複数のデータベース操作を実行したい場合に有用。
Jean Boussier
同Changelogより
つっつきボイス:「Jean Boussierさん(byrootとcasperisfineも同一人物)が概念実証用の#50793からこのwith_connection
を切り出してマージしたそうです」「ActiveRecord::Base.connection
にブロックを与えられるようになっていますね↓」「with_connection
の中では同じコネクションが使われるようになったっぽい」
# activerecord/lib/active_record/connection_handling.rb#L261
+ def with_connection(&block) # :nodoc:
+ connection_pool.with_connection(&block)
end
# activerecord/lib/active_record/transactions.rb#L211
def transaction(**options, &block)
- connection.transaction(**options, &block)
+ with_connection do |connection|
+ connection.transaction(**options, &block)
+ end
end
「最初のテストコードはwith_connection
ブロックの中ではactive_connection?
がtrueでブロックを抜けるとfalseになる、次のテストコードはwith_connection
ブロック内で複数接続してもコネクションが変わらないようになっていますね↓」「このプルリクだけだと意図やユースケースがよくわからないな〜」「#50793をチラ見した限りでは、スレッドやfiberに絡んだActiveRecord::Base.connection
周りの最適化の一環のようですね」
# activerecord/test/cases/connection_handling_test.rb
# frozen_string_literal: true
require "cases/helper"
module ActiveRecord
class ConnectionHandlingTest < ActiveRecord::TestCase
unless in_memory_db?
test "#with_connection lease the connection for the duration of the block" do
ActiveRecord::Base.connection_pool.release_connection
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
ActiveRecord::Base.with_connection do |connection|
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
assert_same connection, ActiveRecord::Base.connection
end
assert_not_predicate ActiveRecord::Base.connection_pool, :active_connection?
end
test "#with_connection use the already leased connection if available" do
leased_connection = ActiveRecord::Base.connection
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
ActiveRecord::Base.with_connection do |connection|
assert_same leased_connection, connection
assert_same ActiveRecord::Base.connection, connection
end
assert_predicate ActiveRecord::Base.connection_pool, :active_connection?
assert_same ActiveRecord::Base.connection, leased_connection
end
後で概念実証の#50793を読んでみました。
コンテキスト
Active Recordは、パフォーマンスとシンプルさの理由、また歴史的にスレッドサポートがなかったこともあって、fiberローカル変数のスレッド内のコネクションをチェックアウトして保持する
ActiveRecord::Base.connection
に非常に強く依存している。具体的には、すべてのリクエストやジョブは、データベース操作を実行する必要が生じるとコネクションを遅延チェックアウトし、リクエストやジョブが完了するまでそのコネクションを保持する。完了した時点で
Executor
フックがコネクションを自動的にプールにチェックイン(返却)する。これによってコネクション管理の問題がほぼ解消されるため、少ないスレッド数で十分メリットを得られ、多数のIOを実行しない圧倒的多数のRailsアプリケーションでは、完全に適切なソリューションである。
ただし、ほとんどの時間をデータベース以外のIO(サードパーティAPIなど)に費やし、はるかに高レベルのコンカレンシーでメリットが生じるアプリケーションの場合、スレッドやfiberとほぼ同数のデータベースコネクションが必要になるため、この戦略が問題となる。ほとんどのコネクションはアイドル状態だが、コネクションがプールからチェックアウトされて別のスレッドやfiberによって掴まれたままになるため利用できなくなる。
目標
大多数のRailsユーザーにとっては現在のモデルが引き続き望ましいと信じている。
ActiveRecord::Base.connection
の振る舞いを変更すると、最終的に大量のコードが破損することになるので、デフォルトのまま変えないようにすべきだと思う。ただし、
ActiveRecord::Base.connection
がリクエストサイクルの終わりまでコネクションを保持し続けず、利用後すぐにコネクションをチェックインして再利用可能にする別のモードをサポートすることは可能だと思う。実装
まず、
ActiveRecord.cache_connection_checkout
を無効にしなければ始まらない。次に、Rails自身やサードパーティのライブラリ、プライベートRailsアプリケーションのコードの多くは
Model.connection.なんちゃら()
に依存しているため、コネクションをキャッシュせずに動かす必要がある。これを解決するには、キャッシュが無効な場合は
Model.connection
からプロキシオブジェクトを返すようにする。これにより、受信したメソッドが新たにチェックアウトされたコネクションに委譲され、再度チェックインされる。詳しくはconnection_pool.rb
のLazyConnectionProxy
を参照。下位互換性を維持しながらこの機能を許可するには、ここがカギとなる。コードでは以後の
Model.connection
呼び出しで同じコネクションインスタンスが返されることを前提にしている可能性が大きいので、完璧ではないものの、ほとんどの場合不要である。また、プロキシはコネクションを保持している間スレッドやfiberでキャッシュされるため、以後の
Model.connection
呼び出しでは、再度チェックインされるまで同じコネクションを返す。これにより、トランザクションをオープンするといったステートフル性の問題のほとんどが解決される(APIがブロッキングされる形になっている限り)。現状
このプルリクは概念実証であり、マージの準備ができているわけではない。主な目的は、いくつかの作業を加えて実行可能か単に不可能かを調査すること。
Active Recordテストスイートのほとんどは、コネクションの切断と再接続を行う3つのテストを除いてSqlite3ではパスする。一般に、この機能からすべてのテストをオプトアウトする必要があったが、その理由を正確に調査する必要がある。
また、キャッシュ依存が強すぎるいくつかのテストスイートではこの機能を無効にする必要があり、これを回避するには少しリファクタリングが必要だが、根本的な非互換性はなさそうに見える。
懸念事項
現在、複数のチェックアウト/チェックインサイクルを引き起こすActive Recordメソッドが多数存在しており、それを最適化するために戦略的に配置された
with_connection do
呼び出しの恩恵を受けられる。しかし主な問題はパフォーマンスであり、根本的な非互換性ではない。もう1つの主な懸念事項はクエリキャッシュである。現状では、チェックアウトキャッシュが無効になっていると、クエリキャッシュはあまり役に立たない(コネクションを再度チェックインするときにキャッシュをクリアする必要があるためキャッシュはほぼ常に空になる)。再び利用可能にするには、大幅なリファクタリングが必要。可能そうに見えるが、決して簡単ではない。
結論
これに関しては少し迷っている。うまくいくとは思うが、すでに概念実証だけでほぼ丸一日を費やしており、すべての問題を解決するにはさらに多くの作業が必要である。
Active Recordのコード自体で必要な実際の変更は比較的小さいが、このオプションで適切に動作するために実際に更新する必要があるのはすべてのテストスイートとサードパーティのコードであり、蓋を開けたらえらいことになりはしないかと心配している。
#50793より
🔗 バイナリカラムの暗号化をサポート
暗号化と復号でバイナリデータを
Type::Binary::Data
で受け渡しするようにする。従来は、バイナリカラムを
ActiveRecord::Encryption::MessageSerializer
で暗号化すると、MySQLやSQLiteでは動作するがPostgreSQLでは動作しなかった。Donal McBreen
同Changelogより
動機/背景
ActiveRecord::Encryption
はバイナリカラムの暗号化を禁止していないが、適切なサポートも存在しない。暗号化と復号を経たデータは文字列に変換される。これは、暗号化層がバイナリデータに対して透過的ではないことを意味する。バイナリデータは
Type::Binary::Data
で渡されるべき。その結果、データがSQLクエリ内で適切にエスケープされなかったり、復号後に正しくデシリアライズされなくなったりする。
ただし、MessageSerializerではエンコードの必要な文字を使わないので、MySQLやSQLiteではたまたま正常に動作するが、カスタムシリアライザを使おうとすると壊れる。
PostgreSQLの場合は、デシリアライズで
Type::Binary::Data
ではなくString
がBytea
型に渡されるため、データのエスケープ解除を試みるときにnullバイトが含まれていると、データが台無しになったりエラーが発生したりする。詳細
このコミットでは、データの暗号化と復号の後で再度シリアライズすることで問題を修正する。テキストデータの場合は何も行わないが、バイナリデータの場合は
Type::Binary::Data
に逆変換される。
同PRより
つっつきボイス:「ActiveRecord::Encryption
でバイナリカラムを暗号化しようとしたらPostgreSQLだとできなかったのね」「バイナリは文字列に変換せずにバイナリのまま扱うようにしたんですね」「bytea
はPostgreSQLのbyte array型のことか」
参考: PostgreSQL 15ドキュメント 8.4. バイナリ列データ型
🔗 puma-devの改修2件
Dockerを使っていない場合、複数のRailsアプリケーションをローカルで開発するときの黄金のパスはpuma-devになる。そこで、
bin/setup
を実行したときにpuma-devのセットアップ方法を表示することにしよう。
同PRより
つっつきボイス:「これは次の#51087と合わせてDHHがpuma-devサポートを追加したんですね: 改修自体はbin/setup
のテンプレートにpuma-dev設定方法のメッセージを追加するシンプルなもの」
# railties/lib/rails/generators/rails/app/templates/bin/setup.tt#L41
puts "\n== Restarting application server =="
system! "bin/rails restart"
+ # 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"
end
development環境でデフォルトで許可されるホストに".test"を追加し、puma-devがスムーズにセットアップされるようにする。
DHH
同Changelogより
# actionpack/lib/action_dispatch/middleware/host_authorization.rb#L22
class HostAuthorization
- ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
+ ALLOWED_HOSTS_IN_DEVELOPMENT = [".localhost", ".test", IPAddr.new("0.0.0.0/0"), IPAddr.new("::/0")]
「こちらもdevelopment環境で.test
で終わるホスト名を使えるようにするシンプルな修正ですね」「.test
って予約済みドメインにあるのか↓」
参考: 予約済みドメイン (.example, .localhost, .test) について | blog.jxck.io
「puma-devは以前取り上げたこともあったけど(ウォッチ20221026)どういうものだったっけ?」「Dockerを使わずにサーバーを複数立ち上げたいときにpuma-devを使うと自動的にホスト名やポートを割り振ったりするので便利だよという感じでDHHが推しているみたいですね」「このあたりは好みもありますけど、自分はローカル環境をあれこれ変えるよりはDockerでやる方がいいかな」
🔗Rails
🔗 Rails 8の計画(Ruby Weeklyより)
つっつきボイス:「もう知られているものもありますが、Rails 8に向けてのプランを記事にまとめたものです」「Rails 8のマイルストーンを元にした記事なのかな」
参考: 8.0.0 Milestone
「Solid QueueがActive Job用のジョブエンジンとして使われるのは既に見てきましたね」「Action Cableのmessage broadcastがRedisを使うものからDBのpub/sub機能を使うものに実装が変更されるらしい」「小ネタだけどPWA(progressive Web App)への対応もありますね」
参考: Solid Queue should be the default Active Job backend for Rails 8 · Issue #50442 · rails/rails
参考: Add DB-backed Action Cable adapter as new default in Rails 8 · Issue #50480 · rails/rails
This is why I'm pushing so hard for PWAs! We're betting all of https://t.co/viKQ8g66rk on that strategy. No native apps, just the very best PWAs you can possibly find. It's why I have PWA tech as my #1 objective for Rails 8. We need to collectively break free from this bullshit.
— DHH (@dhh) January 3, 2024
「お、Railsのアセットパイプラインのデフォルトとして長らく使われてきたSprocketsをいよいよ正式にPropshaftに置き換えるらしい」
参考: Make Propshaft the default asset pipeline in Rails 8 · Issue #50444 · rails/rails
参考: § 3 Sprocketsの利用法 -- アセットパイプライン - Railsガイド
「Rails 8にlanguage server(LSP)が追加されるのは期待できそう👍」「ベンチマークツールも標準装備する予定なんですね」
参考: Add a default LSP for better editor/IDE support in Rails 8 · Issue #50453 · rails/rails
参考: Add a built-in benchmark tool · Issue #50451 · rails/rails
「RuboCop設定やGitHubとの統合、HTTP/2サポート強化のためにRack 3が必須になる話やKamalなどもこれまで見てきましたね(ウォッチ20240117)」「全体として、Solid Queue導入やPropshaftへの置き換えはそれなりの変更だけど、Rails 8のアーキテクチャは大きくは変わってはいない感じかな」
参考: Require Rack 3 · Issue #50565 · rails/rails
参考: Setup Kamal by default for new applications · Issue #50441 · rails/rails
個人的にはBasic認証のジェネレータが入るのがちょっと嬉しいです😂: システムテストも生成してくれるといいですね。
参考: Add basic authentication generator · Issue #50446 · rails/rails
参考: § 11.1 BASIC認証 -- Rails をはじめよう - Railsガイド
🔗 dynamoid: AWS DynamoDB向けのORM gem(Ruby Weeklyより)
つっつきボイス:「AWSのフルマネージドNoSQL DBとしておなじみのDynamoDB用ORM: この手のORマッパーはいろいろありますよね」
参考: オブジェクト関係マッピング - Wikipedia
参考: 【公式】Amazon DynamoDBとは(マネージド NoSQL データベース)| AWS
「READMEがかなり詳しく書かれていますね」「read_capacity
をモデル側で設定したり、capacity_mode: :on_demand
のような指定もできるのね↓」「これはどういうものでしょうか?」「DynamoDBのテーブル作成・設定変更時にread / writeで割り当てるキャパシティを指定するもので、provisionedモードならread / writeキャパシティの数値、ondemandモードならモードの指定だけすれば良いです: もちろん割り当てを大きくするとそれだけコストも上がります」
# 同リポジトリより
class User
include Dynamoid::Document
table name: :awesome_users, key: :user_id, read_capacity: 5, write_capacity: 5
end
table capacity_mode: :on_demand
参考: 読み取り/書き込みキャパシティモード - Amazon DynamoDB
「こういうORMが欲しくなるのはわかるけど、DynamoDB backedなモデルに対してActive Recordと同じように柔軟なアクセスを気軽にやってしまうと、RDBMSとは違うDynamoDB独自のインデックス仕様をうまく使えずに課金額がものすごいことになったりする可能性があるので要注意です」「ご利用は計画的にということですね」
🔗 その他Rails
#RailsTip: With #Zeitwerk as the default autoloader, the recommended way to create custom root namespaces is by pushing directories to the `main` autoloader.
Assuming you want to have `app/services` under the `Services` namespace, here's what you should do: pic.twitter.com/r75cRFiaHv— Ruby on Rails (@rails) February 7, 2024
つっつきボイス:「前にも見た気がしますが、Rails公式がポストしたZeitwerkのノウハウです」「そうそう、これはZeitwerkでディレクトリを増やすときにありがちなパターンで、空のServices
モジュールを宣言しておかないとエラーになるヤツですね(ウォッチ20230328)」
🔗Ruby
🔗 runruby.dev: bundlerも実行できるブラウザRuby環境(Ruby Weeklyより)
- サイト: RunRuby.dev
つっつきボイス:「Ruby Playgroundや先週のemirb(ウォッチ20240227)に続いてこれもWebAssemblyベースでRubyを実行できるサイトだけど、bundle install
を実行できるのが面白いですね👍」
参考: Ruby Playground
参考: emirb - irb on ruby-wasm-emscripten + xterm-pty
🔗言語/ツール/OS/CPU
🔗 Gitの知られざる機能(Ruby Weeklyより)
gitのあまり知られていないオススメ設定、あまり知られていない機能、最近入った新機能など紹介。わいわい / “So You Think You Know Git - FOSDEM 2024” https://t.co/nkHo08mOd9
— matsuu@充電期間 (@matsuu) February 15, 2024
つっつきボイス:「FOSDEMっていうイベント初めて見た」「サイトを見ると2024年度はベルギーのブリュッセルで開催されていますね」「スケジュールを見るとメイントラックが2つの構成だけど、本当のメインであるdevroomがめちゃくちゃ多い↓」「全部チェックするのは物理的に無理でしょうね」「Rubyのdevroomもあった」
参考: FOSDEM 2024 - Home
参考: FOSDEM 2024 - Schedule
「動画は45分なので飛ばし見するか」「Gitは今19歳なのね」「git blame -w
でホワイトスペースの差分を非表示にできるのか」「git maintenance start
って知らなかった」「他にもいろいろ便利技がありそうですね」
参考: Git - git-maintenance Documentation
参考: Git - git-blame Documentation
つっつき後に調べると、発表者が動画の内容をブログで公開してくれていました↓🎉
参考: Git Tips 1: Oldies but Goodies
参考: Git Tips 2: New Stuff in Git
参考: Git Tips 3: Really Large Repositories
今回は以上です。
バックナンバー(2024年度第1四半期)
週刊Railsウォッチ: Turbo Nativeアプリ、書籍『Everyday Rails Testing with RSpec』新版執筆開始ほか(20240227後編)
- 20240221前編 form_withのmodelオプションへのnil渡しが非推奨化、Dockerfileでjemallocが有効にほか
- 20240207後編 aws-sdk-rubyの全gemにRBSファイルが追加ほか
- 20240206前編 Pumaのデフォルトスレッド数変更、Rails 1.0をRuby 3.3で動かすほ
- 20240125後編 RailsコントローラのparamsはHashではない、ruby-enumほか
- 20240123前編 Railsの必須Rubyバージョンが3.1.0以上に変更ほか
- 20240119後編 Ruby 3.3でYJITを有効にすべき理由、Turbo 8の注意点8つほか
- 20240117前編 Rails 8マイルストーン、2023年のRails振り返り、Solid Queueほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)