Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Active Storageのミラーアップロードが非同期に、Rackアプリを手作りほか(20230829前編)

こんにちは、hachi8833です。WEB+DB PRESSの最終号をKindleで購入しました。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗Rails: 先週の改修(Rails公式ニュースより)

やっと公式に追いつきました。

🔗 Webpackerガイドへのリンクを英語版Rails Guidesのindexページから削除

動機/背景
Webpackerは現在非推奨となり、Rails7アプリケーションでは使われていない。そのため、Railsガイドのindexページでwebpackerを参照する必要はなくなった。

詳細
このプルリクは、ガイドのindexページからWebpackerへの参照を削除する。

同PRより


つっつきボイス:「WebpackerガイドへのリンクがついにRails Guidesトップのインデックスから削除されるそうです」「お、ついに消えるのか」「はい、日本語のRailsガイドでも現行のWebpackerガイドそのものは残す予定です」「なるほど、今回の改修もリンクを消しただけですね: Webpackerの代わりにShakapackerを使いましょうみたいな情報もなくなるのかな?」「edgeGuidesのアセットパイプラインガイドにはShakapackerの記述がありますね」

Rails: Webpacker v5からShakapacker v6へのアップグレードガイド(翻訳)

🔗 Action Mailerのプレビューが未定義の場合にプレビューでわかりやすく表示するようになった

動機/背景

アプリケーションが対応するプレビューを定義せずにメーラーを定義すると、GET /rails/mailersリクエストは空の<body>要素を持つページを返す。

ページが完全に空になっていると、空のリストとエラーが無視された「失敗」状態とを区別しにくいため、混乱を招く可能性がある。

同様に、ActionMailer::Previewのサブクラスが定義されているが、アクションが何も宣言されていない場合、レスポンスに含まれるページがほぼ空になってしまう。

詳細

このコミットは、上の両方のシナリオに対して空の状態メッセージを表示し、Action Mailer Basicsガイドへのリンクも提供する。

このコミットでは、振る舞いを効果的にカバーするために、Mailerプレビューテストのカバレッジも拡張し、rails-dom-testingのアサーションを利用している。

同PRより


つっつきボイス:「Action Mailerのプレビューが未定義の場合にプレビューが未定義であることがわかるような表示に変えた、なるほど」「プレビューに情報がないとエラーなのか未定義なのかわかりにくいですよね」


後でmainブランチで改修後の未定義プレビューを表示してみました。

  • Rails 7.0.7の場合

  • mainブランチ(7.1.0.alpha)の場合

参考: §2.6 メールのプレビュー -- Action Mailer の基礎 - Railsガイド

🔗 Content-Typeヘッダーに空文字を渡した場合のエラーを変更

動機/背景

このプルリクを作成した理由は、この変更(1071a39)以降、クライアントが空のContent-Typeヘッダーでリクエストを送信した場合に、actionpack/lib/action_dispatch/http/mime_type.rb166行目"undefined method 'rstrip' for nil:NilClass"というNoMethodErrorが発生しているため。

このクライアントは仕様に沿っていないと思うが、このコミットの前ならActionDispatch::Http::MimeNegotiation::InvalidTypeエラーが発生し、受け取るレスポンスは「500 Server Error」ではなく「406 Not Acceptable」だっただろう。

NoMethodErrorはエラートラッキングシステムを混乱させ、より具体的な無効なMIMEタイプのエラーを無視できてしまう。

詳細

このプルリクは、MIMEタイプの検索を変更し、rstripの呼び出しに安全なぼっち演算子&.を使うことで、クライアントが空白のContent-Typeヘッダーでリクエストを行った場合に対応する。

追加情報

次のコードスニペットには、現在のmainブランチで問題を再現できるサンプルが含まれている。

▶クリックすると展開します
# frozen_string_literal: true

require "bundler/inline"

gemfile(true) do
  source "https://rubygems.org"

  git_source(:github) { |repo| "https://github.com/#{repo}.git" }

  gem "rails", github: "rails/rails", branch: "main"
  gem "rack", "~> 2.0"
end

require "action_controller/railtie"

class TestApp < Rails::Application
  config.root = __dir__
  config.hosts << "example.org"
  config.secret_key_base = "secret_key_base"

  config.logger = Logger.new($stdout)
  Rails.logger  = config.logger

  routes.draw do
    get "/" => "test#index"
  end
end

class TestController < ActionController::Base
  include Rails.application.routes.url_helpers

  def index
    render plain: "Home"
  end
end

require "minitest/autorun"
require "rack/test"

class BugTest < Minitest::Test
  include Rack::Test::Methods

  def test_returns_success
    get "/", {}, 'CONTENT_TYPE' => ''

    assert last_response.server_error?
    assert (last_response.status == 500)
  end

  private
    def app
      Rails.application
    end
end

同PRより


つっつきボイス:「リクエストのContent-Typeヘッダーが空だとMIMEタイプの処理でNoMethodErrorエラーになってたのか: NoMethodErrorはたしかにわかりにくい」「以前取り上げた#48397の改修(ウォッチ20230628)で生じたバグだったんですね」「nilエラーの対応なので、修正はぼっち演算子&.を導入しただけ↓」

# actionpack/lib/action_dispatch/http/mime_type.rb#L164
      def lookup(string)
        # fallback to the media-type without parameters if it was not found
-       LOOKUP[string] || LOOKUP[string.split(";", 2)[0].rstrip] || Type.new(string)
+       LOOKUP[string] || LOOKUP[string.split(";", 2)[0]&.rstrip] || Type.new(string)
      end

Rubyのぼっち演算子はRailsの`Object#try`より高速(翻訳)

🔗 Action Packのfixture_file_uploadfile_fixture_uploadにリネーム

動機/背景

テストハーネスのfile_fixtureヘルパー(およびfile_fixture_pathコンフィグ値)と、統合テストハーネスのfixture_file_uploadの命名が食い違っているため、常に混乱と驚きが引き起こされる。

詳細

Active Supportの方が普及が進んでいるため、このコミットではfixture_file_uploadメソッドをfile_fixture_uploadにリネームした。これにより、file_fixturefile_fixture_pathの語順が一致するようになる。

後方互換性を維持するため、fixture_file_uploadのエイリアスを宣言することで将来に備える(または将来のどこかで削除する)。

同PRより


つっつきボイス:「メソッド名の不統一を修正するために、多数派のfile_始まりのメソッド名に寄せてリネームしたのか、なるほど」「リネーム前のメソッドもエイリアスで残すので互換性は大丈夫ですね」

🔗 Active Storageのミラーアップロードを非同期に

クローズ: 47918

文脈

Active Storageのミラーリングは実に重宝する。ただし、storage.ymlでミラーサービスを定義すると、ミラーへのアップロードが実際にはインラインで行われるため、ActiveStorage::MirrorJobが使われない。

ミラーの個数をnとすると、(ActiveStorage::MirrorJobが使われずに)インラインでn回処理されるため、サービスへのアップロードが遅くなる。

修正

このコミットより前は、ミラーサービスにファイルがアップロードされると、個別のサービス(プライマリおよびミラー)へのファイルのアップロードは同期的に行われていた。

def upload(key, io, checksum: nil, **options)
  each_service.collect do |service|
    io.rewind
    service.upload key, io, checksum: checksum, **options
  end
end

このコミットは、プライマリサービスにはファイルを同期的にアップロードし、その後ミラーサービスにはファイルを非同期でアップロードするジョブをエンキューする形で振る舞いを変更する。

def upload(key, io, checksum: nil, **options)
  io.rewind
  primary.upload key, io, checksum: checksum, **options
  mirror_later key, checksum: checksum
end

同PRより


つっつきボイス:「Active Storageにミラーのアップロード機能があるとは知らなかったけど、ミラーへのアップロードは非同期でできる方がいいですね👍」「プライマリへのアップロードは同期的で、ミラーへのアップロードが非同期になるんですね」

参考: Rails API ActiveStorage::Service::MirrorService
参考: Rails API ActiveStorage::MirrorJob

🔗Rails

🔗 Rails Foundationの"Rails Luminary Awards"(Rails公式ニュースより)


つっつきボイス:「Rails FoundationがRails Luminary Awardsという賞を創設したそうです」「今年10月に行われる例のRails World(ウォッチ20230427)の一環のようですね」「Ruby Prizeを連想しますね」「このフォームでノミネートできるのね」「誰が受賞するかな」

参考: Rails World - 2023

🔗 Rackアプリケーションをゼロから手作りするガイド

rack/rack - GitHub


つっつきボイス:「記事には"Rackのことが今までよくわかってなかったのでRackアプリケーションを作ってみた"とありますね」「Rackアプリは基本的にcallメソッド↓(env引数を取り、statusheadersbodyの値からなるnon frozen arrayを返す)を実装して結果を返すようになっていれば、それで動くようになりますよ」

# 同記事より
# app/app.rb

class App
  def call(env)
    [200, {}, ["Hello World"]]
  end
end

「resolvedというのがそのサンプルアプリだそうです↓」

thoughtbot/resolved - GitHub

「Rackアプリ手作り企画は昔からちょくちょく見かけますし、一度は学びのためにやってみるといいと思います👍: Rackアプリは、routes.rbに直接書いても動くぐらいシンプルなので、ヘルスチェックのようにRailsを使うまでもないシンプルなアプリを書くときにも使えます」「そういえばSinatraもRackベースのフレームワークですね」

sinatra/sinatra - GitHub

🔗 Railsコアメンバーによる「モンキーパッチ反対」(Ruby Weeklyより)


つっつきボイス:「RailsコアメンバーのEileenさんの記事です」「モンキーパッチの苦労がしのばれる、よさそうな記事👍」

「モンキーパッチは使わないに越したことはないし、モンキーパッチに頼らざるを得ないRailsプロジェクトは管理がちゃんとしていなかったり、下手をするとモンキーパッチのテストすら行われていなかったりする可能性もあるので、よほど凄腕の古参エンジニアがいるのでなければモンキーパッチは避けたい」「たしかに」

「Railsアプリにモンキーパッチを当てるよりも、ShopifyやGitHubがよくやっているように、本家Railsにプルリクを投げて反映するのが、本来は正当な方法」「そういえばだいぶ前に、GitHubが自分たちの古いRailsアプリケーションをアップグレードするために、ものすごく苦労してモンキーパッチを解消した話がありましたね↓」

参考: Upgrading GitHub from Rails 3.2 to 5.2 - The GitHub Blog -- 2018年の記事です

「記事によると、モンキーパッチの代替方法であるrefinementは相当遅いらしい」

RubyのRefinement(翻訳: 公式ドキュメントより)

「記事に貼られていた"責任あるモンキーパッチ"↓、これも参考になりそうな記事👍」「Module#prependを使う、gemのバージョンをチェックするなど、モンキーパッチがどうしても避けられない場合の方法がいろいろ紹介されていますね」

参考: Responsible Monkeypatching in Ruby | AppSignal Blog

「GitHubのような大規模Railsアプリでモンキーパッチを完全になくすのは難しいかもしれませんが、少なくとも正しい方法で最小限のモンキーパッチを当てつつ、Rails本家にもissueとプルリクを投げるのが望ましいですね」「記事にもあるように、モンキーパッチを削除する手順も事前に定めておくのも大事」

🔗 Railsでセッションcookieを設定しないようにする方法


つっつきボイス:「短い記事です」「RailsをAPIサーバーとして使うときにセッションcookieを止めたいという話ですね」

# 同記事より
# application_controller.rb
after_action lambda {
  request.session_options[:skip] = current_api_token.present?
}

def current_api_token
  # 認証がAPIトークン経由の場合にのみ値を返す
end

lambdarequest.session_options[:skip]に値を設定することで個別にセッションcookieを止められるんですね: なおセッションcookieを止めるだけならCookie関連のミドルウェアを削除する方が早いと思いますし、そもそもRailsアプリをAPIモードで新規作成した場合はデフォルトでセッションミドルウェアをインストールしませんが、いろんな方法があることは知っておいてよさそう」

参考: § 3.2.3 ミドルウェアを削除する -- Rails と Rack - Railsガイド
参考: §4.4 セッションミドルウェアを利用する -- Rails による API 専用アプリケーション - Railsガイド


前編は以上です。

バックナンバー(2023年度第3四半期)

週刊Railsウォッチ: ArelでCAST関数サポート、webdrivers依存を解消、YJIT高速化ほか(20230824後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース

Ruby Weekly


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。