Discourse を Zeitwerk にアップグレード

Rails 6 には、zeitwerkclassic の 2 つの自動読み込みモードが搭載されています。このプルリクエスト DEV: Upgrading Discourse to Rails 6 by KrisKotlarek · Pull Request #8083 · discourse/discourse · GitHub では、移行期として Rails をバージョン 6.0.0 にアップグレードし、クラシックな自動読み込みを使用しました。Zeitwerk への切り替えを試みるのは興味深いでしょう。

Zeitwerk は、Ruby 向けの効率的でスレッドセーフなコードローダーです。プロジェクトが命名規則に従っていれば、Zeitwerk は適切なファイルを検出し、requirerequire_dependency を一切必要とせずに、必要に応じてまたは事前にファイルをロードできます。また、この記事 https://weblog.rubyonrails.org/2019/2/22/zeitwerk-integration-in-rails-6-beta-2/ によると、アプリケーションのパフォーマンスがわずかに向上する可能性もあります。

動作させるためには、いくつかのステップを実行する必要があります。

  1. Rails の命名規則に従うために、いくつかのクラス名を変更します。例えば、ファイル canonical_url.rbCanonicalURL ではなく CanonicalUrl クラスを定義する必要があります。同様に、ファイル ondiff.rbONPDiff ではなく Onpdiff クラスを定義する必要があります。別のアプローチとして、プロジェクトにカスタムインフレクターをフックすることもできますが、規則に従う方がよいと考えます - GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby · GitHub

  2. 前項と同様に、慣例により validations ディレクトリに存在するカスタムバリデーションは Validations モジュールでラップする必要があります。さらに、一部のバリデーションは EachValidator から継承しており、名前空間なしでアクセス可能であるべきです。これらを別のディレクトリに移動し、自動読み込みパスに追加する予定です。

  3. 全ての require_dependency を削除し、プロジェクトが正常に動作することを確認します。

  4. 全ての require を削除し、Discourse が正常に動作することを確認します。

  5. 全てのプラグインが必要な依存関係にアクセスできることを確認します。現時点ではその方法がわかりません。まずはプラグインなしで Discourse が動作するようにすることを目指します。

もちろん、まだ解決すべき不明点がたくさんあります。進捗状況は随時更新していきますが、もし興味があれば、こちら Commits · KrisKotlarek/discourse · GitHub で試作を始めています。

Zeitwerk の実装に欠点がある場合や、見落としている点がある場合は、お知らせください。

Zeitwerk に関する進捗がありましたが、アプローチを変更しました。当初の計画は、Discourse が Zeitwerk のファイル名規約に従っていない箇所をすべて修正することでした。いくつかの修正を行った後、これは氷山の一角に過ぎないことに気づきました。もしその道を進むなら、プルリクエストが読みづらくなり、マスターへのマージも確信を持って行えなくなるでしょう。例えば、通常のディレクトリ配下のすべてのジョブクラスは Regular 名前空間を持つべきであり、OnceoffScheduled も同様です。

少し立ち止まって、革命的なアプローチよりも進化的なアプローチについて考えることにしました。

Zeitwerk 規約に従っていないすべてのファイルをカバーするカスタム Inflector を導入するのがより良いと判断しました。最大の利点は、その小さな変更をデプロイでき、Zeitwerk に満足し、パフォーマンスの低下がないことが確認できたら、合理的なサイズの小さなプルリクエストでファイルごとに規約の修正を始めていけることです。

カスタム Inflector では解決できない問題もいくつか見つかったため、動作させるために追加の修正を行いました。

プルリクエストは現在進行中ですが、この段階では、Zeitwerk とデフォルトのプラグインを用いて Discourse を実行し、すべての仕様テストを実行し、ベンチマークを問題なく実行できます。

まずはすべての仕様テストが通る安定した状態にすることを優先しました。これで、require_dependency を一つずつ自信を持って削除し始め、公式プラグインもテストできます。すべてが準備でき次第、この投稿でベンチマーク結果を共有します。

現時点で進捗に興味がある方は、このドラフトのプルリクエストをご覧ください - DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

最も重要なファイルは、このカスタム Zeitwerk Inflector です - DEV: Upgrading Discourse to Zeitwerk by KrisKotlarek · Pull Request #8098 · discourse/discourse · GitHub

プラグインを動作させるために、いくつかの小規模なプルリクエストを作成する必要がありました。それらがマージされれば、Discourse のスペックが通るようになると思います。

また、Rails 6.0.0 の Classic autoloader と Rails 6.0.0 の Zeitwerk でのパフォーマンスも確認しました。

テスト Classic Zeitwerk パーセント
categories-50 32 26 81.25
categories-75 37 29 78.38
categories-90 47 35 74.47
categories-99 67 49 73.13
home-50 30 29 96.67
home-75 37 31 83.78
home-90 44 40 90.91
home-99 67 52 77.61
topic-50 35 35 100.00
topic-75 36 36 100.00
topic-90 48 36 75.00
topic-99 57 58 101.75
categories_admin-50 51 48 94.12
categories_admin-75 62 50 80.65
categories_admin-90 89 66 74.16
categories_admin-99 135 101 74.81
home_admin-50 48 47 97.92
home_admin-75 58 49 84.48
home_admin-90 67 64 95.52
home_admin-99 101 81 80.20
topic_admin-50 48 48 100.00
topic_admin-75 55 49 89.09
topic_admin-90 63 65 103.17
topic_admin-99 92 69 75.00
load_rails 2617 2165 82.73
rss_kb 282428 315684 111.78
pss_kb 270491 303504 112.20

結果は常に一貫しているわけではないため、これらの数値は参考程度に受け取ってください。

ここでの中央値のばらつきが少し奇妙ですね。なぜ結果がこれほど大きく変動するのか不思議です。

ローダーが何らかの影響を与えるとは思っていませんでした。

もう一度試してみました。結果は以下の通りです。

テスト クラシック Zeitwerk パーセント
categories-50 25 25 100.00
categories-75 26 26 100.00
categories-90 37 33 89.19
categories-99 57 48 84.21
home-50 26 26 100.00
home-75 27 28 103.70
home-90 38 35 92.11
home-99 60 50 83.33
topic-50 27 26 96.30
topic-75 35 27 77.14
topic-90 41 33 80.49
topic-99 54 50 92.59
categories_admin-50 48 50 104.17
categories_admin-75 60 61 101.67
categories_admin-90 76 71 93.42
categories_admin-99 122 122 100.00
home_admin-50 47 46 97.87
home_admin-75 58 55 94.83
home_admin-90 66 63 95.45
home_admin-99 99 121 122.22
topic_admin-50 50 49 98.00
topic_admin-75 62 50 80.65
topic_admin-90 72 65 90.28
topic_admin-99 103 74 71.84
load_rails 2675 2216 82.84
rss_kb 279924 315240 112.62
pss_kb 267659 303026 113.21

別のベンチマーク手法を試してみましょう。1 時間程度実行するような、より多くの反復回数についてはどうでしょうか?さらに、最良の結果を採用する代わりに、各実験の平均値を比較する方法はいかがでしょうか。それにより、より一貫性のある数値が得られる可能性があります。ご意見をお聞かせください。

プラグインに関するプルリクエストをご覧いただくと、多くの修正がグローバル名前空間内での検索に関連していることがお分かりいただけると思います。

私は以下のようなコードを変更しました。

module ::Jobs
  class TranslatorMigrateToAzurePortal < Jobs::Onceoff

以下のように。

module ::Jobs
  class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff

この解決策について一つ気になった点があります。なぜZeitwerk以前は動作していたのでしょうか。動作するはずがないのに動作しているという疑問は、常に厄介なものです :slight_smile:

おそらく、クラシックなオートローダーの説明(https://guides.rubyonrails.org/autoloading_and_reloading_constants_classic_mode.html#resolution-algorithms)にその答えがあると考えています。「見つからない場合、アルゴリズムはcrefの祖先チェーンを上にたどります」

Zeitwerkはより厳格です。修正前にコードを読み込もうとすると、Jobs::Jobs::Onceoff が見つからないと警告されました。

Samは、プルリクエスト FIX: Use top-level namespace for base classes · discourse/discourse-prometheus-alert-receiver@ef9c238 · GitHub で、 < ::Jobs::Onceoff を使う代わりに、単に < Onceoff とすればよいと提案しました。彼は正しいです。名前空間なしでも動作することを確認しました。:: を付けることは、Discourse Coreのクラスから継承することを明示的に示すことになりますが、どちらの方法でも問題ありません。

コードがこれほど近接している場合、この読み方は非常に優れていると思います:

module ::Jobs
  class TranslatorMigrateToAzurePortal < Onceoff

しかし、コードが離れてくると、明示的に記述する方が理にかなっています。例えば:

module ::Jobs
  [ 50 lines omitted]
  class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff

とはいえ、私はまだ迷っていますので、どちらでも構いません。::Jobs::Onceoff は短く、非常に明示的ですので、当面はこれで進めましょう。

これをマージできるようになるのを心から楽しみにしています。@kris.kotlarek さん、これでマージ可能な状態になりましたか?タイミングも非常に良く、ちょうどベータ版をリリースしたばかりです。

最新の master をそれに対してリベースし、まだ動作することを確認します。今日はそれを行います。

@sam 準備は整ったと思います。最新の master を基にリベースを行い、Webauthn に小さな調整を加えました。
以下の 3 つを確認しました:
ローカルでサーバーを起動し、期待通りに動作するか軽く操作を確認
スペックを実行
すべての公式プラグインをダウンロードし、プラグインのスペックが通ることを確認(プラグインの調整を先にマージする必要があります)

いいね、これをマージします。どうなるか楽しみですね!

プラグインの修正もマージすることはできますか?そうしないと、プラグイン付きで Discourse を実行しようとした際に失敗します。

ぜひマージしてください!

これでマージされました!プラグイン作成者で困っている方がいれば、新しい専用トピックでご報告ください!

素晴らしい仕事です、クリス!!!