Rails 6 默认提供两种自动加载模式:zeitwerk 和 classic。在该拉取请求 DEV: Upgrading Discourse to Rails 6 by KrisKotlarek · Pull Request #8083 · discourse/discourse · GitHub 中,我将 Rails 升级到了 6.0.0 版本,并暂时使用 classic 自动加载器作为过渡方案。尝试切换到 Zeitwerk 会很有意思。
Zeitwerk 是一个高效且线程安全的 Ruby 代码加载器。只要项目遵循命名规范,Zeitwerk 就能找到正确的文件并按需或预先加载它们,而无需任何 require 或 require_dependency。此外,根据这篇文章 https://weblog.rubyonrails.org/2019/2/22/zeitwerk-integration-in-rails-6-beta-2/,它还可能为应用程序带来轻微的性能提升。
为了实现这一目标,我需要完成以下几个步骤:
-
修改部分类的名称以符合 Rails 命名规范。例如,文件 canonical_url.rb 应定义 CanonicalUrl 类,而不是 CanonicalURL。同样,文件 ondiff.rb 应定义 Onpdiff 类,而不是 ONPDiff。另一种方法是为项目挂载自定义推断器(inflector),但我认为遵循规范可能是更好的选择 - GitHub - fxn/zeitwerk: Efficient and thread-safe code loader for Ruby · GitHub
-
与上一点类似,按照规范,位于 validations 目录中的自定义验证应包裹在 Validations 模块中。此外,某些验证继承自 EachValidator,应无需命名空间即可访问。我计划将它们移至单独的目录,并添加到自动加载路径中。
-
移除所有 require_dependency,并确保项目正常运行。
-
移除所有 require,并确保 Discourse 正常运行。
-
确保所有插件都能访问所需的依赖项。目前我还不知道如何实现这一点。我打算先让 Discourse 在不加载任何插件的情况下运行。
当然,仍有许多未知问题需要解决。我会持续向您汇报进展。如果您有兴趣查看,我在此处开始尝试:Commits · KrisKotlarek/discourse · GitHub
如果您发现实施 Zeitwerk 存在任何弊端,或认为我遗漏了某些内容,请随时告知。
我在 Zeitwerk 方面取得了一些进展,但我改变了方法。我最初的计划是修改 Discourse 中所有不符合 Zeitwerk 文件名约定的地方。在修复了几处问题后,我意识到这仅仅是冰山一角,并且注意到如果继续沿着这条路走,拉取请求(PR)将难以阅读,也难以有信心地合并到主分支。例如,常规目录下的所有作业类都应具有 Regular 命名空间,Onceoff 和 Scheduled 同理。
我决定稍微退一步,思考一种更具演进性而非革命性的方法。
我决定最好引入一个自定义的 Inflector,以覆盖所有不符合 Zeitwerk 约定的文件。这样最大的好处是,我们可以先部署这一小改动,一旦我们对 Zeitwerk 感到满意且没有出现性能下降,就可以开始以合理的小型拉取请求逐个修复约定文件。
我发现有些问题无法通过自定义 Inflector 解决,因此我进行了额外的修复以确保其正常工作。
拉取请求仍在进行中,但在此阶段,我已经能够使用 Zeitwerk 和默认插件运行 Discourse,运行所有测试用例,并顺利执行基准测试。
我首先希望达到所有测试用例都通过的稳定状态。现在,我可以有信心地逐个移除所有的 require_dependency,并测试官方插件。一旦一切准备就绪,我将在此帖子中分享基准测试结果。
目前,如果您想了解进展,可以查看这个草稿 PR: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 自动加载器以及 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 |
结果并不总是一致,因此请谨慎看待这些数据。
sam
(Sam Saffron)
4
这里的中间值不一致程度有点奇怪,我不明白结果为何波动如此之大
没想到加载器竟会有任何影响
我又试了一次,结果如下:
| 测试项 |
Classic |
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 |
我们可以尝试另一种基准测试方法。你觉得增加迭代次数、让测试运行一个小时怎么样?此外,与其取最佳结果,不如比较每次实验的平均值。这样可能会得到更稳定的数据。你怎么看?
在我为插件提交的拉取请求中,您会注意到许多修复都涉及全局命名空间中的搜索问题。
我将类似以下的代码:
module ::Jobs
class TranslatorMigrateToAzurePortal < Jobs::Onceoff
修改为:
module ::Jobs
class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff
这个解决方案让我有些困惑:为什么在 Zeitwerk 之前它能正常工作?当某段代码能运行但实际上不应该运行时,这个问题总是很棘手 
我认为我在经典自动加载器的描述中找到了一个潜在答案(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 核心类,不过两种方式都可以。
sam
(Sam Saffron)
7
我认为当代码靠得这么近时,这样读起来很顺畅:
module ::Jobs
class TranslatorMigrateToAzurePortal < Onceoff
但如果代码开始分散,显式声明会更有意义……例如:
module ::Jobs
[ 50 行省略 ]
class TranslatorMigrateToAzurePortal < ::Jobs::Onceoff
不过,我对此持中立态度,所以两种写法我都可以接受。::Jobs::Onceoff 足够简短且非常明确,因此我们现在可以先采用这种方式。
sam
(Sam Saffron)
8
我很希望能尽快合并这个。@kris.kotlarek 现在是否可以合并了?时机正好,因为我们刚发布了 beta 版本。
让我把最近的 master 分支变基到那里,并确保它仍然能正常工作,我今天会完成。
@sam 我认为我们可以开始了。我已基于最新的 master 分支进行了变基,并对 Webauthn 做了小幅调整。
我检查了以下三项:
在本地运行服务器并点击测试,确保其按预期工作
运行测试规范
下载所有官方插件,并确保插件的测试规范通过(我们需要先合并针对插件的调整)
我们是否也可以合并插件的修复?否则,如果您尝试运行带有插件的 Discourse,将会失败
sam
(Sam Saffron)
16
现在已合并!如果有任何插件作者在此遇到困难,请在新的专用主题中告诉我们!
干得漂亮,Kris!!!