2024 年 Clojure 状态调查!中分享您的想法。

欢迎!请在关于页面查看有关如何使用此功能的更多信息。

+13 投票
多方法

一个简单的重现方式

Clojure 1.10.2
user=> (defmulti f identity)
#'user/f
user=> (defmethod f :default [x] nil)
#object[clojure.lang.MultiFn 0x4bb8855f "clojure.lang.MultiFn@4bb8855f"]
user=> (dotimes [_ 100000] (f (byte-array 1000000)))

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

如果您调整数字以避免 OOM 错误,您将看到一个持续的泄漏。

罪魁祸首似乎在于 MultiFn.findAndCacheBestMethod 缓存与默认方法匹配的每个值。

1 个答案

+1 投票

我不知道这是否是泄漏,更多的可能是一个设计假设,即分派值的数量是有界的。

曾有一段时间我们没有缓存产生 :default 情况的分派值,这导致 :default 非常慢(这是多方法长期以来被认为“慢”的主要原因)。在 1.8 中,http://dev.clojure.org/jira/browse/CLJ-1429 改变了这一点,极大地提高了许多实际程序的性能。

可能有一块中间地带,不确定。

对于这种场景,使用协议是否是避免缓存溢出的更好选择?或者,也许可以手动实现 `case` 并完全避免使用多方法(multimethods)和协议?
我关注的并非由于开发者错误导致的意外泄露,而是关于如果允许(潜在的恶意或高度随机的)用户输入触发多方法(multimethods)可能会引起实际问题的这个事实。

我相信大多数Clojure程序员都非常小心,如果这一点很明确,人就会再三思考是否应有默认方法或找到关闭多方法输入的其他方式——但这一点并不明确。我已专业使用Clojure多年,并不知道:default 匹配是缓存的。

例如,memoize 会导致类似的问题,但我认为在那种情况下(你可能会说明显)而多方法是有点微妙。

我不知道是否有解决办法,当然速度很重要,而且我相信在大多数情况下都是好的。
假设这是可能的,一个用于 клатч,row defmulti 选项的问题吗?这是否能够解决缓存问题?(缓慢但未缓存的多方法查找是否比安全有界限的缓存开销更大?)
需要注意的是,这种缓存发生在调度函数的输出上,而不是在多方法或调度函数的输入上。最常用的调度函数有像`type`、`class`这样的,或者用于从一个映射中提取字段的键。虽然它们都有无限的范围,但通常比域的范围更有限制。我不记得有人在过去5年+的时间里报告过这个问题。协议也使用缓存(每个调用位置),实际上(除了ns/var表),这些是Clojure运行时内两个主要的有状态对象。

可以添加很多可能的控制措施到这个中,非常类似于memoize(缓存)。在后一种情况下,我们将它推到core.memoize/core.cache中,而将更简单的版本保留在core中。我不想支持或反对任何事情,需要一些评估。创建了一个问题报告
当然,不应该有太多的不同的调度值。但有时候,调度值并不是开发者想的那个值。

我在生产环境中意外地遇到了一个内存泄漏,当时我正在使用关键字作为调度函数。我的意图是通过对第一个参数中字段的值进行调度,但不幸的是,当找不到关键字时,关键字作为函数处理了多方法第二个参数作为调度值。

以下是简单的再现示例

```
(defmulti foo :type)
(defmethod foo :default [_ _] nil)
(foo {} (Math/random))
```

我的意图是拥有以下内容
```
(defmulti foo (fn [x y]
                (:type x)))
```

请注意,我损坏的代码没有造成任何功能错误,只是由于多方法的缓存机制导致了内存泄漏。
即使它适用于调度结果,结果也可以作为外部世界或其他程序的输入提供,除非程序员已经确保它是有限的,否则不能假定它是有界的。我认为这与协议缓存有很大不同,由于类型与值的对比,想象一个基于count进行调度的多方法,你可以为0、1、2进行特殊化,然后为其他情况进行默认处理,或者有一个在HTML表单中选择值的:type字段,:default可以作为无效选择时的回退。
仅为列举一些可能导致人们困惑的意外问题的案例清单之一。最近有一个服务出现了内存泄漏,原因是升级了一些依赖项。问题组合是 plumatic schema + ring swagger + compojure api。在该次提交中,Schema 已更改以返回匿名函数:[https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0](https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0) ,而 ring swagger 则使用 `identity` 作为分派函数 [https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178](https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178) 。因此,在 compojure api 调用中使用查询参数中的 map 时,方法缓存中的匿名函数 `ring.swagger.coerce/custom-matcher` (以及 `time-matcher`)会在每次请求时耗尽。我们通过降级 schema 并提高迁移到 spec/malli 的优先级来解决了我们的问题,但我想写下一条备注,即有一些库在野中使用 `identity` 作为多方法的分派函数,这似乎是一个我们应该注意的红旗。
我不知道这个,ring-swagger 的事是我的做法。我很乐意在那里修复这个问题。
...