- Ruby / Rails関連
週刊Railsウォッチ(20201110前編)Rails 6.1 RC1がリリース、Railsアプリに最適なEC2インスタンスタイプ、n_plus_one_control gemほか
こんにちは、hachi8833です。
- 各記事冒頭には⚓でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
- 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
- お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙇
⚓Rails: 先週の改修(Rails公式ニュースより)
以下の公式更新情報より見繕いました。
つっつき直前に、6.1.0rc1タグができていることに気づきました↓。
つっつきボイス:「rc版が出るということはリリースはそう遠くなさそうかな」「6.1.0rc1タグはできていてzipもアップロードされているけど、releaseタブにはまだ6.1.0rc1が置かれてないんですよ」「rcはタグを付けるだけという運用なのかな?」「今まであまり気にしてなかった...」
「以前の6.0.3.rc1を見るとPre-release
と表示されてReleaseに置かれている↑」「ということはrc版もリリースされたらReleaseタブに出るんでしょうね」「今回の6.1.0.rc1はまだPre-release
と表示されてないから、まだ動く可能性がありそう」「rc1のアナウンスが出たら表示チェックしてみます」
その後、つっつき翌日の金曜日に6.1 RC1がリリースされました。
ちょっとわかりにくいのですが、Releasesを開いて「Show 2 newer tags」をクリックすると6.1 RC1が表示されます↓。
⚓ ペンディング中のマイグレーションがある場合に画面/コンソール/ログに出力
# activerecord/lib/active_record/migration.rb#L146
def initialize(message = nil)
- if !message && defined?(Rails.env)
- super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate RAILS_ENV=#{::Rails.env}")
- elsif !message
- super("Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate")
- else
- super
- end
+ super(message || detailed_migration_message)
end
+
+ private
+ def detailed_migration_message
+ message = "Migrations are pending. To resolve this issue, run:\n\n bin/rails db:migrate"
+ message += " RAILS_ENV=#{::Rails.env}" if defined?(Rails.env)
+ message += "\n\n"
+
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
+
+ message += "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}\n\n"
+
+ pending_migrations.each do |pending_migration|
+ message += "#{pending_migration.basename}\n"
+ end
+
+ message
+ end
end
マイグレーションがペンディングされていたらUIやコンソールやログに表示し、どのマイグレーションがいくつペンディングされているかをすぐ把握できるようにする。
同PRより大意
つっつきボイス:「ペンディングされているマイグレーションを具体的に表示できるようになったんですね👍」「コミットやプルリクのmerge順次第では、未実行のマイグレーション番号が常に最新の番号とは限らないけど、マイグレーション番号単位で具体的に未実行のものが分かるようになるのか」
「タイトルのoutstandingって『秀逸な』という意味かと思ったら『未処理の』という意味もあるそうです」「つまりペンディング中ですね」
⚓ 新機能: paramごとにエンコーディングを設定できるようになった
従来はエンコード(アクションのすべてのパラメータをASCII_8BITとしてエンコードする)をスキップできた。
今回の変更で、アクションの任意のパラメータでparam_encoding
を指定できるようになった。
この変更はGitHubでパラメータのエンコーディングを扱うときに#40124の不正なエンコーディング検出をサポートする(この変更は大きい)。
これにより、POSTのbodyパラメータがリクエストのURLパラメータと異なる可能性がある点も配慮する必要がある。
同PRより大意
# actionpack/lib/action_controller/metal.rb#L138
- def self.binary_params_for?(action) # :nodoc:
+ def self.custom_encoding_for(action, param) # :nodoc:
false
end
ウォッチ20200928で取り上げた#40124と関連しているそうです。
つっつきボイス:「デフォルトのエンコーディングはASCII_8BITなのはこれまでどおりだけど、UTF-8なども指定して取り出せるようになったみたい」「Rafaelさんがコメントでハッシュ探索が1段増えるとパフォーマンスに影響するのではと指摘してますね↓」
この変更によるパフォーマンスのインパクトはどうなる?パラメータ値ごとに毎回ハッシュ探索しているが、paramsのハッシュに値が200個以上あったらハッシュ探索も200回行われることになる。この変更で、このメソッドを使わないユーザーも複雑さがO(1)からO(N)に増加したりしないだろうか?
同PRのコメント(rafaelfranca)より
「続きの#40465ではcustom_encoding_for
メソッドを使わない場合に呼び出しを回避している↓」「機能を使わない場合でもパフォーマンスが落ちないよう配慮したんですね」
# actionpack/lib/action_controller/metal/parameter_encoding.rb#L18
- def custom_encoding_for(action, param) # :nodoc:
- @_parameter_encodings[action.to_s][param.to_s]
+ def action_encoding_template(action) # :nodoc:
+ if @_parameter_encodings.has_key?(action.to_s)
+ @_parameter_encodings[action.to_s]
+ end
end
「custom_encoding_for
メソッドを自分で使うところはあまり想像できませんが、プルリクの意図はそういう感じでしょうね」
⚓ crossorigin
属性を使うとリソースが2回フェッチされる問題を修正
スクリプトやCSSを(それぞれ
javascript_include_tag
やstylesheet_link_tag
で)読み込むときにcrossorigin
属性が適用されると、現在のRailsでは一部のブラウザでこれらのリソースを2回読み込みが発生してしまう。理由はlink
ヘッダーのpreload
ディレクティブやリソース自身のcrossorigin
がブラウザでリソースの再利用にマッチする必要があるため。たとえばビューで以下のタグを使うとする。
<%= javascript_include_tag("[...snip]react.production.min.js", crossorigin: "anonymous") %>
javascript_include_tag
はこのリソースをLink
HTTPヘッダーにプッシュするが、現在はcrossorigin
属性を無視している。
これによって上述のダブルフェッチとなる。
Chromeではwarningも表示される。
A preload for 'https://cdnjs.cloudflare.com/ajax/libs/react/17.0.0/umd/react.production.min.js' is found, but is not used because the request credentials mode does not match. Consider taking a look at crossorigin attribute.
このプルリクでは
Link
ヘッダーのディレクティブに、リソース自身に渡されたものと同じcrossorigin
値を含めるように変更し、これによってプリロードされたリソースをブラウザで再利用できるようにする。
anonymous
を生成するcrossorigin: true
は既存のヘルパーで行われるので、その振る舞いをここにも複製した。
同PRより大意
つっつきボイス:「2回フェッチするってブラウザ側で起きるのか!」「そういえばcrossorigin
属性というものがありますね」「javascript_include_tag
で作られるHTMLの問題らしい」
参考: HTML crossorigin
属性 - HTML: HyperText Markup Language | MDN
crossorigin 属性は、
<audio>
,<img>
,<link>
,<script>
,<video>
の各要素で有効であり、 CORS への対応を提供し、したがって要素が読み取るデータのために CORS リクエストの構成を有効にします。要素によっては、属性は CORS 設定属性になります。
developer.mozilla.orgより
⚓ 新機能: connecting_to
メソッド
コンソールをreadonlyモードで起動されるようにするコードをGitHubで使おうと思ったが、そのためのpublic APIがなかったのでこのメソッドを追加した。
通常と異なるデフォルトコネクションが必要だが、そのコネクションをブロックで呼んでない場合がたまにある(コンソールを読み出しモードで起動するなど)。このプルリクは、アプリケーションコードで使うconnected_to
の振る舞いを維持しつつ、起動時にスクリプトで特定のコネクションを設定する機能を追加する。
同PRより大意
つっつきボイス:「このconnecting_to
はコントローラーやモデルロジック内で使うことは想定していなさそうです」「たとえばRailsコンソールのようなインタラクティブ端末内で以後の入力コードのデフォルト接続先を変えたいときなどに、このconnecting_to
でできるということらしい」「へ〜」「Railsコンソールでconnecting_to
を使って接続先を変えれば、readonlyな状態で操作したいなどのケースではコンソール操作でconnected_to
ブロックを書き忘れてもconnecting_to
で設定したコネクションが使われるので、そのような用途に便利なんでしょうね」「なるほど」
# activerecord/lib/active_record/connection_handling.rb#L175
# 特定のコネクションを使う。
#
# このメソッドは特定のコネクションが使われるようにするときに有用。
# (コンソールをreadonlyモードで起動するときなど)
#
# このメソッドは`connected_to`と違ってブロックでyieldしないので、requestで用いることは推奨されない。
def connecting_to(role: default_role, shard: default_shard, prevent_writes: false)
if legacy_connection_handling
raise NotImplementedError, "`connecting_to` is not available with `legacy_connection_handling`."
end
prevent_writes = true if role == reading_role
self.connected_to_stack << { role: role, shard: shard, prevent_writes: prevent_writes, klass: self }
end
「connected_to
はブロックの中でしかコネクションが有効にならないんですけど、Railsコンソールのように自由に操作できる環境だとconnected_to
のブロックを律儀にreadonlyを付けて書くとは限らないので、上のコード例のように書くことでコネクションに縛りをかけた状態で操作できるということだと思います」「readonlyモードを使いたくてこのメソッドを作ったのかもしれないと想像してみました」
⚓ データベースタスク作成でyamlを読めない場合に警告を出すようになった
つっつきボイス:「database.ymlの警告を何度も出すのではなく最初に1回だけ出すようにしたらしい」「for_each(databases)
に変わったところからするとマルチプルデータベース関連の修正っぽいですね」「たしかにdatabases
が複数形になってる」
# activerecord/lib/active_record/railties/databases.rake#L28
- ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ ActiveRecord::Tasks::DatabaseTasks.for_each(databases) do |spec_name|
desc "Create #{spec_name} database for current environment"
task spec_name => :load_config do
db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name)
ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
end
end
end
「こちらのメッセージもマルチプルデータベース向きになってる↓」「database.ymlが書式レベルでは正しくても内容が正しくないような場合に、従来は最初のステップがパスして次のステップでうまくいかなかったのを、最初のステップで警告が出るようにチェックを加えたんでしょうね」
# activerecord/lib/active_record/tasks/database_tasks.rb#L144
- def for_each
+ def setup_initial_database_yaml
return {} unless defined?(Rails)
- databases = Rails.application.config.load_database_yaml
+ begin
+ Rails.application.config.load_database_yaml
+ rescue
+ $stderr.puts "Rails couldn't infer whether you are using multiple databases from your database.yml and can't generate the tasks for the non-primary databases. If you'd like to use this feature, please simplify your ERB."
+
+ {}
+ end
+ end
「テストを見ると、ERBの中身が入っていないような場合にdb_create_with_warning
してる↓」「余分なエラーが減るのは嬉しいですね」
# railties/test/application/rake/dbs_test.rb
test "db:create and db:drop show warning but doesn't raise errors when loading YAML with alias ERB" do
app_file "config/database.yml", <<-YAML
sqlite: &sqlite
adapter: sqlite3
database: db/development.sqlite3
development:
<<: *<%= ENV["DB"] || "sqlite" %>
YAML
app_file "config/environments/development.rb", <<-RUBY
Rails.application.configure do
config.database = "db/development.sqlite3"
end
RUBY
db_create_with_warning("db/development.sqlite3")
end
最初のdatabase.ymlを一旦読み込み、タスクを作成できない場合はwarningを出す。
マルチプルデータベースではRailsアプリケーションの起動前にdatabase.ymlを読み込んでタスク生成を試みる。これはERBをストリップする必要があるが、これはRailsコンフィグを読み出している可能性があるため。
#36540のようないくつかのケースではERBが複雑すぎて#35497で自分たちが使ったDummyCompilierでは上書きできない。複雑さが原因のときは、database.ymlからデータベースタスクを推測できないというwarningを単に出力している。
自分はこの作業をやりながら、同じwarningが何度も出力されるのを避けられるよう、database.ymlを最初に1度だけ読み込むようにコードを更新することに決めた。なお自分のテストではパフォーマンスへの影響はなく、単にエラーをどこかに保存せずに済むようにしたかった。それにこの方がクリーンに思える。
なおこの変更で既存の実行中タスクは壊れない(単に
db:create:other_db
のようなマルチDB向けタスクが生成されなくなる)。database.ymlが実際に読み取り不可能な場合は、通常のrakeタスク呼び出し中に落ちる。
修正対象: #36540
同PRより大意
⚓ TimeWithzone
とDateTime
の比較を修正
問題点
#40413でTimeWithZone
をDateTime
と比較したときに丸めの問題が生じた(失敗するテストを書いたにもかかわらずパスした)。
修正方法
この問題はtime_instance.to_f
を用いたときに発生した(特定のケースで精度が不足することがあった)。Time
インスタンスの作成をtime_instance.to_r
でRationalにすることでこの問題の解決を試みた。これによってパフォーマンスがわずかに落ちるが引き換えに精度を得られる。
同PRより大意
つっつきボイス:「プルリクで参照している#40413↓の見出しにあるけど、Time.zone.at
をTimeWithZoneで呼び出すときとTime
で呼び出すときで振る舞いが異なる場合があったとは」
「TimeWithZoneの場合はto_r
することで修正している↓」「Rationalに変換してるんですね」「DateTimeの場合はこれまでどおりto_f
すべきなのか」「この問題よく見つけたな〜」
# #L
def at_with_coercion(*args)
return at_without_coercion(*args) if args.size != 1
# Time.at can be called with a time or numerical value
time_or_number = args.first
- if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+ if time_or_number.is_a?(ActiveSupport::TimeWithZone)
+ at_without_coercion(time_or_number.to_r).getlocal
+ elsif time_or_number.is_a?(DateTime)
at_without_coercion(time_or_number.to_f).getlocal
else
at_without_coercion(time_or_number)
end
end
alias_method :at_without_coercion, :at
alias_method :at, :at_with_coercion
⚓Rails
⚓ 🌟Railsアプリに最適なEC2インスタンスタイプとは(Ruby Weeklyより)🌟
つっつきボイス:「TechRachoの翻訳記事↓でもお世話になっている、ベンチマークに強いNoah Gibbsさんの記事です」「Railsに最適なEC2インスタンス!」「そこそこ長い記事ですね」「駆け足で読んでみますか」
「burstableなT4インスタンスは短期間のベンチマークだと速いけど長期間だと速度が落ちると書かれてる」「たしかにT系インスタンスはバーストパフォーマンスインスタンスのCPUクレジット問題があるので長期間のベンチマークのようなものに使うべきではないでしょうね: 長期実行だとCPUクレジットを使い切ってしまうという基本的な話」「なるほど」
参考: バーストパフォーマンスインスタンスの CPU クレジットとベースライン使用率 - Amazon Elastic Compute Cloud
「C5などのC系はCPUを重視するコンピューティング最適化インスタンスで、こちらはバーストクレジットがないので動かし続けられます」「RubyにRactorが正式に入ったらRailsでもC系インスタンスを使えるかもしれないと書かれてる」
参考: Amazon EC2 C5 インスタンス | AWS
「記事ではDiscourseのRailsアプリ↓が現実に近いということでベンチマークに使っている」「Railsのプロセスあたりのスレッド数のような具体的な数値が書かれてますね」「Discourseアプリの場合、EC2の2xlargeインスタンスだと1プロセスあたり6スレッドにしたときにプロセス10個でvCPUやメモリをだいたい使い切る感じらしい」「いつだったかRubyKaigiでこのあたりの話を聞いたような気がする🤔」
「記事ではAWSのARMインスタンスやdedicated instance(ハードウェア専有インスタンス)にも触れてますね」「やったことないけどARMでCRubyって動くのかな?」「CRubyのコンパイルぐらいまではできるかもしれないですね」
参考: Amazon EC2 A1 インスタンス | AWS
参考: Amazon EC2 ハードウェア専有インスタンス | AWS
「T系やC系やM6gインスタンスは、特殊なケースならともかく、一般にはRails向けに万人にはおすすめしない、特にスピードテストでは結果が正しくなくなると書かれてる」「なるほど!」
「記事の途中で『M4かM5のどちらか』と書かれてる」「たぶんRailsのメモリ使用量だとそのぐらいが適切で、かつCPUバーストクレジットもないから、特にこだわりがなければM4かM5に絞られるということなんでしょうね」「記事の終わりの方を見ると、コスパも見ると実はオンデマンドインスタンスならM4よりM5の方が安いみたいなことが書かれてる」「なおオンデマンドインスタンスは、スポットインスタンスと違ってコストが固定されます」
参考: Amazon EC2 スポットインスタンス | AWS
参考: オンデマンドインスタンスの料金 - Amazon EC2 (仮想サーバー) | AWS
「インスタンスタイプの一覧↓を見ると『コンピューティング最適化』とか『ストレージ最適化』とかいろんなカテゴリがあるんですね」「M4やM5というとメモリが多めの汎用インスタンスという印象でしたけど、最近だと『メモリ最適化』に含まれるR系がそれ用なのかな」
参考: インスタンスタイプ - Amazon EC2 | AWS
「急いで読んだ範囲では、思った以上に経験に裏打ちされた非常に具体的な記事で、読んでてとても納得がいきます👍」「AWS事情もみっちり解説されてて、親切丁寧さがスゴい」「もっと概念的な話かと思ったら具体的ですね」「Noah Gibbsさんらしい説得力のある記事」
「AWSのEC2でRailsを運用したことがない、または経験の少ないRailsエンジニアはこの記事を読んでおくといいんじゃないかと思います: おそらくそういう人にとって知らない用語がいっぱい出てくると思うので調べながら読むことになるでしょうけど」「この記事翻訳してもらっていいですか?」「はい、この後でオファー出します」「これはいい記事❤️」
超久しぶりに🌟を進呈します。おめでとうございます。
その後Noah GibbsさんからOKをいただきましたので、近々同記事の翻訳を公開します。ご期待ください。
追記(2020/12/10): 公開しました。
⚓ Dockerコンテナ内のSSHコンフィグをWSL2で使い回す(Ruby Weeklyより)
つっつきボイス:「Dockerコンテナ内のSSHコンフィグをWSL2で使い回すということは、$HOME/.sshディレクトリをボリュームでマウントしようねという話かなと思ったら本当にそうだった↓」「予想通りでした😆」
# 同記事より
volumes:
- type: bind
source: ${HOME}${USERPROFILE}/.ssh
target: /home/${DEV_USER:-abapa}/.ssh
⚓ rubocop-rails_config: Rails公式のRuboCop設定と同じスタイルを使える(Ruby Weeklyより)
つっつきボイス:「officialという言葉があったのでRails公式のRuboCopコンフィグかなと思ったら、Rails公式のRuboCopコンフィグを持ってきてそれと同じスタイルにできるというgemだそうです」「inherit_gem
で指定できるのね↓」
# 同リポジトリより
inherit_gem:
rubocop-rails_config:
- config/rails.yml
「自分で公式のコンフィグをコピペすれば同じことができますけど、gemでインストールする意義って何だろう?🤔」「gemでやれば公式のスタイルが更新されたときに追従できそうですね」「たしかに」「おそらくRails公式のRuboCopコンフィグは今後もRuboCopのバージョンアップにも追従するでしょうから、RuboCopをバージョンアップしやすくなるかもしれない」「Railsのスタイルに常に合わせておきたい人によさそう」
⚓ n_plus_one_control: N+1クエリをマッチャーで調査するgem(Ruby Weeklyより)
RSpecとminitestのどちらでも使えるそうです。
つっつきボイス:「Rails技術記事↓でお馴染みのEvil Martiansがスポンサーになっている、テストのマッチャーでN+1クエリを検出するツールの記事です」
「N+1クエリ問題が本当に起きているかどうかを確認するには最終的にコードを動かさないといけないんですけど、このgemはこんなふうに↓『N=2のときは5回』『N=5のときは8回』というふうにテストで実際に回してヒントを出してくれるgemみたいですね」「あ〜なるほど」
「↓以下のような感じでスケールファクターを渡して実際にテストを回してみると、N+1クエリ問題が起きていればNの増加に応じて回数が指数関数的に増大するはず: 上のスクショではNと回数の差が常に3なのでN+1は起きていませんが」
# 同リポジトリより
# You can specify the RegExp to filter queries.
# By default, it only considers SELECT queries.
expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)
# You can also provide custom scale factors
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
「N+1クエリ問題の検出によく使われるbullet gem↓はコードをチェックすることで自動検出しますけど、このn_plus_one_controlは実際にいくつかのスケールでクエリを投げていろんな実行回数を出して、それを人間が見てチェックするということでしょうね」「なるほど」
「n_plus_one_controlは入れただけで自動的にN+1クエリ問題を検出してくれるものではなく、N+1クエリ問題が起きている疑いがある場合に自分で当たりをつけて発生箇所を探すためのツールだと思います👍」
⚓ Meilisearch: Rust製の全文検索エンジン
つっつきボイス:「Rust製のMeilisearchという全文検索エンジンをRailsで使ってみたという記事です」「外部サービスではなくてMeiliSearchサーバーを自分で動かしてやるものみたい」「手元の辞書を見ると、Meiliは人の名前っぽい雰囲気でした」
「タイポや同義語もよしなに解釈してくれるらしい↓」「漢字もサポートしてる!」「漢字はサポートされてても日本語の文法までサポートされているかどうかはわかりませんけど」「あ、たしかに」「軽くググった限りではMeiliSearchの日本語記事が見当たらないので、日本語はこれからなのかもしれませんね」「他のエンジンのtokenizerも使い回したりできたら嬉しいかも」
- 機能
- インクリメント検索(50 msec以内の応答)
- 全文検索
- タイポ許容(タイポやスペルミスを解釈する)
- さまざまな検索・フィルタオプション
- 漢字のサポート
- 同義語のサポート
- インストール・デプロイ・メンテが容易
- 全文を返す
- 高度なカスタマイズ機能
- RESTful API
「全文検索機能はあまり使ったことがないけど、Meilisearchは割とホットなプロジェクトみたい」「★が9900個もあってリポジトリも活発みたいです」「こういうものがあることを知っておくとよいでしょうね」
後でMeiliSearchの公式サイトも見つけました↓。公式ツイートはつっつき後に見つけたものです。
- サイト: MeiliSearch
Version 0.16 is officially out! ⚡️️With it, we aim to simplify the use and setup of MeiliSearch even more 😎. With implicit index creation, you now only need to add documents to start searching. 🔎 🔥https://t.co/VOLw7M65K1
— MeiliSearch (@meilisearch) November 5, 2020
追いかけボイス: 「MeiliSearchの同義語サポート↓、one-wayだけじゃなくmulti-wayに同義語検索するようにもできるのがなかなか面白い: 頑張ってDB作るとかなり柔軟&有用な検索が作れそう」
参考: Synonyms | MeiliSearch Documentation v0.16
前編は以上です。
バックナンバー(2020年度第4四半期)
週刊Railsウォッチ(20201028後編)RuboCop 1.0.0 stable版がリリース、Ruby DSLのGUIフレームワークGlimmer、Keycloakほか
- 20201026前編 Shopifyのerb-lint gem、Form Objectを使いやすくするyaaf gem、railsrcの機能追加ほか
- 20201021後編 webpack 5リリースでWebpacker対応開始、AWS Lambda Extensions発表、Pythonにマクロ構文追加提案ほか
- 20201020前編 Percona Toolkitは優秀、Active Admin非公式ガイド、Railsをリアクティブにするガイドほか
- 20201013後編 ruby-type-profilerがtypeprofにリネーム、AWS API Gatewayの実行ログは便利、M5Stackほか
- 20201012前編 Railsの隠し機能routing visualizer、action_args gem、N+1用goldiloader gemほか
- 20201006後編 Rubyの
defined?
キーワード、Ractorベースのジョブスケジューラ、Caddy Webサーバーほか - 20201005前編 Ruby 2.7.2がリリース、Shopifyのモジュラー化gem「packwerk」、stimulus_reflexほか
今週の主なニュースソース
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。