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

週刊Railsウォッチ: Rubyに新しくRJITがマージされた、Shopifyのタスク管理gem maintenance_tasksほか(20230322)

こんにちは、hachi8833です。今回のRailsウォッチは単品でお送りします。

週刊Railsウォッチについて

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

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

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

🔗 Active Record Encryptionのダイジェストアルゴリズムを変更可能になった

このプルリクは、Active Record Encryptionのダイジェストアルゴリズムを設定する新しいオプションを追加する。Rails 7.1からはSHA-256を新しいデフォルトとして設定し、従来バージョンではSHA-1を設定する。

これはこの問題への再挑戦である(#42929を参照)。この最初のプルリクのことを忘れていたが、マージされないままだった🙄。

Rails 7は新しいデフォルトでSHA-256を使うようになった。ただしこのバグのせいで、Active Record Encryptionは、この変更で意図せず修正されるまでSHA-1を使い続けていた。この修正は最近のものなので、7.1より前についてはSHA-1をデフォルト設定にするのが合理的である。

このことから、Active Record Encryptionでダイジェストアルゴリズムを修正するメカニズムが有用である理由が示される。ダイジェストを変更すると既存の暗号化済みメッセージを復号できなくなるので、Railsで新しいダイジェストアルゴリズムを選ぶときにダイジェストが不用意に変更されないようにしたい。

これは以下の新しいオプションで設定できる。

config.active_record.encryption.hash_digest_class = OpenSSL::Digest::SHA256

最初にレポートした@boomer196#44540のトラブルシュートで大きく貢献した@georgeclaghorn、そしてこのバグを露呈する変更(#44540)を行った@matthewdに感謝したい。
cc: @matthewd
同PRより


つっつきボイス:「Active Record暗号化機能のデフォルトのダイジェストアルゴリズムがSHA-1だったんですね」「Rails 7.1からはSHA-256になるけど、SHA-1も引き続き選べるようにしておかないと、既存のSHA-1ダイジェストを使って暗号化したデータがすべて復元できなくなってしまう」

参考: SHA-1 - Wikipedia
参考: Active Record と暗号化 - Railsガイド

🔗 rails new--css sassでdartsass-railsが使われるようになった

Railsアプリを生成するときに--css sassを指定すると、常にcssbundling-railsのSassが使われる。今ならdartsass-railsがあるのだから、--css tailwindを指定すればtailwindcss-railsが使われるのと同じように、Node.jsの不要なdartsass-railsを使う方がよいだろう。
同PRより


つっつきボイス:「今まではrails new--css sassを指定してもdartsass-railsじゃないSassがインストールされてたのか」「dartsass-railsならバイナリ実行版のDartsassがインストールされるのに、cssbundling-railsでインストールされたらもったいない」「--css tailwindは既にできてたんですね」「7.1がリリースされたら関連の過去記事を更新します」

dartsass-rails README(翻訳)

cssbundling-rails README(翻訳)

🔗 has_many :through関連付けで複合キーの設定をサポート

このプルリクは、#47230と同様に、複合query_constraintsコンフィグを利用するhas_many :items, through: :items_source関連付けをサポートする。

この修正内容は、query_constraintsの範囲内で行っている作業とほぼ同じ。foreign_keyがArrayの場合に、foreign_keyが単一の値であることを期待するコードが動くようにする。
この場合は、属性リストから複合query_constraintsのすべての部分を除外するようにthrough_scope_attributesを変更している。
追加したテスト
壊れたユースケースをカバーするテストを追加したが、壊れたロジックは別のバグ#47485(これは#47534にも依存している)で隠蔽されるため、現時点ではmainブランチでパスする。

そういうわけで、上記2つのプルリクを先にレビューしてもらえるとありがたい。それによって、このプルリクのテストが実際にmainブランチで失敗するようになり、提案する修正を加えればパスするようにできる。
別案として、上記のバグがあるにもかかわらずmainで失敗するテストを考える方法もあるが、これは新しいモデルのセットアップが必要なので、むやみにテストモデルを増やすよりもバグを修正する方にしたい。
同PRより


つっつきボイス:「今回サポートが追加された複合キーはhas_many :throughが対象なんですね」「複合外部キーは#47230でマージ済みですし、複合キーの対応が進んでいるのは嬉しい👍」

参考: Composite key - Wikipedia -- 複合キー

🔗 データベース設定のカスタムハンドラを登録可能になった

データベース設定をカスタムメソッドに応答させたい場合に、カスタムハンドラを登録可能にするメカニズムを追加した。これは、Rails以外のデータアダプタやVitessのように、標準のHashConfigUrlConfigと異なる設定を行いたい場合に有用。

以下のデータベースYAMLで、primaryデータベースではUrlConfigオブジェクトを作成し、animalデータベースではCustomConfigオブジェクトを作成したいとする。

development:
  primary:
    url: postgres://localhost/primary
  animals:
    url: postgres://localhost/animals
    custom_config:
      sharded: 1

カスタムハンドラを登録するには、最初にカスタムメソッドを持つクラスを作成する。

class CustomConfig < ActiveRecord::DatabaseConfigurations::UrlConfig
  def sharded?
    custom_config.fetch("sharded", false)
  end

  private
    def custom_config
      configuration_hash.fetch(:custom_config)
    end
end

次にその設定をイニシャライザで登録する。

ActiveRecord::DatabaseConfigurations.register_db_config_handler do |env_name, name, url, config|
  next unless config.key?(:custom_config)
  CustomConfig.new(env_name, name, url, config)
end

アプリケーションを起動すると、:custom_configキーを持つ設定ハッシュがCustomConfigオブジェクトになり、sharded?に応答するようになる。Active Recordがこのカスタムハンドラを利用すべき条件は、アプリケーションが処理しなければならない。
Eileen M. Uchitelle and John Crepezzi
同Changelogより


つっつきボイス:「Vitessって何だろうと思ったらMySQL互換のデータベースクラスタなんですね↓」「直接にはVitessに対応するためのような感じですが、従来は事実上HashConfigUrlConfigしか設定に使えなかったのを、データベース設定用のクラスを書いて差し替え可能にすることで、Vitess以外のカスタムハンドラも登録可能になった👍」「今までだとクラスにモンキーパッチを当てるような形でカスタマイズするしかなかったんですね」「従来はポリモーフィックに設定できそうに見えて設定できなかったのが、今回ポリモーフィックに設定可能になったということだと思います」

参考: Vitess | Scalable. Reliable. MySQL-compatible. Cloud-native. Database.
参考: Kubernetes Forum@ソウル、YouTubeの本番で利用されるVitessのセッションを紹介 | Think IT(シンクイット)

参考: Rails API ActiveRecord::DatabaseConfigurations::HashConfig
参考: Rails API ActiveRecord::DatabaseConfigurations::UrlConfig


  • configs_for:include_replicas引数が削除された。今後は:include_hidden引数を使うこと。
    Eileen M. Uchitelle

  • カスタムハッシュキー経由で設定を探索できるようになった

カスタム設定を登録した場合や、ハッシュが特定のキーにマッチする設定を探索したい場合に、configs_forconfig_keyを渡せるようになった。
たとえば、db_configvitessというキーがあれば、このキーにマッチするデータベース設定ハッシュを探索できるようになる。

ActiveRecord::Base.configurations.configs_for(env_name: "development", name: "primary", config_key: :vitess)
ActiveRecord::Base.configurations.configs_for(env_name: "development", config_key: :vitess)

Eileen M. Uchitelle
同PRより

「この#47536は、明らかに上の#47522と関連する改修ですね」「こちらもVitessが例に使われていますね」

参考: Rails API config_for -- Rails::Application

🔗 ActionMailer::TestHelperdeliver_enqueued_emailsが追加された

  • ActionMailer::TestHelperdeliver_enqueued_emailsが追加された。このメソッドはエンキューされたすべてのメールジョブを配信する。
def test_deliver_enqueued_emails
  deliver_enqueued_emails do
    ContactMailer.welcome.deliver_later
  end
  assert_emails 1
end

同Changelogより


つっつきボイス:「Action Mailer本体の改修かと思ったら、TestHelperの改修だった: assert_enqueued_jobsというテストメソッドが既にあるので、それと同じようなインターフェイスで使えるdeliver_enqueued_emailsを追加したようですね↓」

ActionMailer::TestHelperにはassert_enqueued_emailsメソッドがあるが、これはメーラーの配信ジョブに対してonlyフィルタ付きでassert_enqueued_jobsを呼び出すだけのものである。このPRではassert_enqueued_jobsのアナロジーに基づいてdeliver_enqueued_emailsメソッドを導入し、配信ジョブのperform_enqueued_jobsをフィルタ付きで呼び出す。
同PR詳細より

参考: Rails API ActionMailbox::TestHelper

🔗 ActionDispatch::Staticのヘッダーを小文字に修正

ActionDispatch::Staticで大文字と小文字が混じったヘッダーが使われていて、小文字のヘッダーとマージされる。これは重複ヘッダーを生成してしまう。これを避けるために、小文字ヘッダーを優先すること。
修正: #47456
同PRより


つっつきボイス:「これは修正すべき: このRackミドルウェアが最終的に出力するHTTPヘッダーは"Accept-Encoding"のように大文字で始まる形になりますが、Rackミドルウェアが内部で保持するヘッダーのキーや値は小文字で統一する前提になっているようで、大文字のキーや値が残っていて同じヘッダーが2回出力されていたのが修正されたんですね」「なるほど」

# actionpack/lib/action_dispatch/middleware/static.rb#L124
            if content_encoding == "identity"
              return precompressed_filepath, headers
            else
-             headers["Vary"] = "Accept-Encoding"
+             headers["vary"] = "accept-encoding"

              if accept_encoding.any? { |enc, _| /\b#{content_encoding}\b/i.match?(enc) }
-               headers["Content-Encoding"] = content_encoding
+               headers["content-encoding"] = content_encoding
                return precompressed_filepath, headers
              end
            end

🔗 Docker関連の改修

# railties/lib/rails/generators/rails/app/templates/Dockerfile.tt#L43
<% if using_node? -%>
# Install node modules
COPY package.json yarn.lock ./
-RUN yarn install
+RUN yarn install --frozen-lockfile

参照: #46953
Rubyのどのredisクライアントもこのパッケージを必要としていない。
同PRより

参照: #47479
アプリの大半はLinuxでデプロイされるので、Gemfile.lockをこれらのプラットフォームで初期化しておくとよいだろう。これにより、GitHub Actionsなどのプラットフォームにプッシュするときにユーザーが手動で追加せずに済むようになる。
同PRより


つっつきボイス:「RailsのDocker関連の改修も増えていますね」「Dockerfileのyarn install--frozen-lockfileがなかったのか」「#47492はM1環境でGemfile.lockにplatform << "--add-platform=aarch64-linux"を追加するけど、コードコメントにはDockerにもaarch64-linuxの追加が必要だろうと書かれていますね」「Dockerのセットアップは各自こだわりがあると思いますが、公式にDocker対応が導入されて叩き台となることで改善が進むのはいいですね👍」

🔗Rails

🔗 sidekiq-iteration: Sidekiqジョブを一時停止・再開可能にする(Ruby Weeklyより)

fatkodima/sidekiq-iteration - GitHub

sidekiq-iterationはSidekiqの拡張で、長時間実行されるジョブの一時停止や再開を可能にし、ジョブの進捗をすべて保存する(ジョブのチェックポイント)。
同リポジトリのREADMEより


つっつきボイス:「こういう実装になっているのは面白い: enumeratorを使う形にすることで、個別のenumrationとenumrationの間でジョブを一時停止・再開可能にしているようですね↓」

# 同リポジトリより
class NotifyUsersJob
  include Sidekiq::Job
  include SidekiqIteration::Iteration

  def build_enumerator(cursor:)
    active_record_records_enumerator(User.all, cursor: cursor)
  end

  def each_iteration(user)
    user.notify_about_something
  end
end

参考: class Enumerator (Ruby 3.2 リファレンスマニュアル)

「enumrationとenumrationの間で実行されるフックを以下のようにカスタマイズできるらしい↓」

# 同リポジトリより
class NotifyUsersJob
  include Sidekiq::Job
  include SidekiqIteration::Iteration

  def on_start
    # Will be called when the job starts iterating. Called only once, for the first time.
  end

  def on_resume
    # Called when the job resumes iterating.
  end

  def on_shutdown
    # Called each time the job is interrupted.
    # This can be due to throttling, `max_job_runtime` configuration, or sidekiq restarting.
  end

  def on_complete
    # Called when the job finished iterating.
  end

  # ...
end

「READMEを見る限りでは、どのタイミングでも一時停止・再開できるわけではなく、あくまでenumrationとenumrationの間で可能だと思うので、これを有効に活用するには個別のenumration間でトランザクションが完結している必要があるでしょうし、個別のenumerationの処理もできるだけ小さく保つ方がいいでしょうね」「なるほど、時間のかかる単発ジョブ向けではないんですね」「ジョブがトランザクションを終えないうちに一時停止したらデータベースがロックされたままになるので、ジョブをそういう形で中断・再開すべきではありません」

🔗 maintenance_tasks: メンテナンスタスクの管理と実行(Ruby Weeklyより)

Shopify/maintenance_tasks - GitHub


つっつきボイス:「ShopifyのRailsエンジンです」「こういうメンテナンスタスク管理が欲しくなるのもわかる: 使い捨てでないメンテナンスタスクはプロジェクトで個別のrakeタスクで手作りされることが多いんですが、maintenance_tasksはそうしたタスクを共通のGUI管理画面で実行・監視できるようにするものですね」「なるほど」

「rakeタスクは現代だと使い勝手があまりよくなくて、たとえば本番環境にコンソールで接続できない場合だとrakeタスクを実行するために工夫が必要になったりしがちですが、GUIでタスクを実行できるとそういうプロジェクトで便利そう」

参考: library rake (Ruby 3.2 リファレンスマニュアル)

「通常のrakeタスクはプロジェクトのlib/以下に置くものですが、このmaintenance_tasksはapp/tasks/maintenance/ディレクトリ以下に素のRubyで書くようになっている↓」「なるほど、rakeタスクではなく、maintenance_tasks独自のタスクなんですね」

# 同リポジトリより
# app/tasks/maintenance/update_posts_task.rb

module Maintenance
  class UpdatePostsTask < MaintenanceTasks::Task
    def collection
      Post.all
    end

    def process(post)
      post.update!(content: "New content!")
    end
  end
end

「通常のrakeタスクと同様に、maintenance_tasksのタスクはコマンドラインでも実行できるようになっている↓」「引数も渡せますね!」

# 同リポジトリより
bundle exec maintenance_tasks perform Maintenance::UpdatePostsTask

bundle exec maintenance_tasks perform Maintenance::ImportPostsTask --csv "path/to/my_csv.csv"

「rakeタスクで作ると、引数を渡すときにシェルで誤って解釈されないようにエスケープが必要な場合があるなど、いろいろハマりどころに注意しないといけないんですよ」「たしかに」「タスク作成や実行によさそう👍: おそらくmaintenance_tasksのタスクはRailsのコンテキストできれいに動いてくれるんじゃないかな」

🔗Ruby

🔗 MJITに代わってRJITがRubyにマージされた(Ruby Weeklyより)

つっつきボイス:「少し前に新しいRJITというJITがRubyにマージされました」「これは驚き!」「@k0kubunさんが満を持して作り上げたんですね」「コミット数もすごい」「pure-Rubyアセンブラからネイティブコードを生成する形で作り直したんですね: これならCコンパイラやRustコンパイラはたしかに不要」「なるほど」「RJITには今後注目していきたいですね👍」

参考: 実行時コンパイラ - Wikipedia -- JIT

このプルリクは、現在のMJIT実装を"RJIT"という新しいJITに置き換える。

  • RJITは純粋なRubyアセンブラからネイティブコードを生成する。
    • MJITは実行時にCコンパイラが必要で、YJITはビルド時にRustコンパイラが必要だが、RJITはどちらも不要。
    • このためRJITのウォームアップはYJITよりは遅くなる可能性があるが、それでもMJITよりはずっと高速。
  • RJITで生成されるコードはYJITのものと極めて近い
    • 実際、多くのメソッドはRustコードからRubyに直接変換されたものである
    • これにより、Ruby VMからMJIT固有の実装を削除してシンプルにできるようになる
    • 必要ならRJITでもYJITの初期実験をある程度行える

動機や詳細についてはFeature #19420を参照。
同PRより

以下はつっつき後に見つけたツイートです。

🔗 rubygemにgem execコマンドがマージ(Ruby Weeklyより)


つっつきボイス:「以前取り上げたgem execコマンドがついにマージされました(ウォッチ20230202)」「これはありがたい🎉」

🔗 Rubyクイズ: 配列操作編(Ruby Weeklyより)


つっつきボイス:「この間もVectorLogicブログのActive Record APIクイズを取り上げました(ウォッチ20230221)が、Ruby配列のクイズも公開されていました」「お、やるか」(一同でつっつく)

「今回は複数回答ありに気をつけたけど、制限時間が短いととやっぱり焦るな〜」「ですよね」「回答を全部入力すると正答と解説を見られるんですね」


なお、VectorLogicからブログの翻訳許可をいただいたので、翻訳記事を今後公開します。クイズそのものは翻訳しませんが、クイズ記事を参照している記事も翻訳したいと思います。


今回は以上です。

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

週刊Railsウォッチ: Wasm Workers Server 1.0、mruby 3.2.0リリース、irbtoolsほか(20230315後編)

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

Rails公式ニュース

Ruby Weekly


CONTACT

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