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"

如果您调整数字以防止OutOfMemory错误,您将看到一个持续的泄漏。

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

1 个答案

+1

我不知道这是否更多的是一个设计假设,即调度值的数量是有限的。

曾经我们没有缓存导致默认情况的调度值,这使得默认变得非常慢(这长期以来一直是多方法被认为“慢”的主要原因)。在1.8中,http://dev.clojure.org/jira/browse/CLJ-1429做了这个更改,并为许多真实程序带来了显著的性能提升。

也许有一个中间地带,不确定。

对于这个场景来说,使用协议是否是避免缓存溢出的更好替代方案?或者只手动实现一个`case`并避免使用多方法和协议?
我的担忧,不仅在于开发人员错误导致的意外泄漏,而且还在于如果允许(可能是恶意的或高度随机的)用户输入触达多方法,这实际上可能会引起真正的问题。

我相信大多数Clojure程序员都非常小心,如果明确表示正在执行这种缓存,那么人们在思考要不要有默认方法或找到其他方法关闭多方法的输入时,会更谨慎一些——但这并不明确。我已经从业多年使用Clojure了,我不知道默认匹配是会被缓存的。

例如,memoize会引发相同类型的问题,但我觉得在那个情况下(你可能会说很明显)更清楚,而多方法对我来说则有一点太微妙了。

我不知道是否有解决方案,当然,快速处理很重要,而且我相信在几乎所有情况下,这都没问题。
假设可能的话,使用clojure.core.cache中的LRU或其他缓存作为defmulti的选项能否解决这个问题?(“慢”的未缓存多方法查找是否比安全界定的缓存的开销慢?)
请注意,这种缓存发生在分发函数的输出上,而不是在多方法或分发函数的输入上。最常见的是像`type`、`class`或用于从映射中提取字段的键。虽然它们都有无界的范围,但它们通常比域有界得多。我不记得自从它更改以来,任何其他人报道过这种情况。协议也使用缓存(按调用的位置),事实上(除了ns/var表之外),这些都是Clojure运行时内部两个主要的有状态事物。

你可能会在这里添加很多控制,与memoize非常相似。在后一种情况下,我们将其推到core.memoize/core.cache中,并保留简单的版本在core中。我对其中任何方面都没有辩解,需要进行一些评估。创建了https://clojure.atlassian.net/browse/CLJ-2626
完全有道理,不应该有太多不同的分发值。但有时分发值并不是开发者所想的。

我不小心在生产环境中使用一个键作为分发函数时遇到了内存泄漏。我的意图是在第一个参数字段中相应的值上进行分发,但错误地处理了键无法找到时的情况,认为该键作为函数将多方法第二个参数视为分发值。

这里是简单的重现。

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

我的意图是
```
(defmulti foo (fn [x y]
                (:type x)))
```

注意,我的有缺陷的代码并没有引起任何功能错误行为,只是由于多方法的缓存机制造成了内存泄漏。
尽管它适用于分发的结果,但这些结果可能是外部世界或其他程序的输入,除非程序员确认这些结果是有界的。我认为这与协议缓存不同,因为类型与值不同,想象一下基于计数进行分发的多方法,你可以针对0、1、2进行特殊化,然后让:default处理其他情况,或者一个:type字段,其值在HTML表单中选择,:default可能是对无效选择的回退。
by
仅为将另一类可能让人意外的问题添加到列表中。最近,由于升级某些依赖项,一个服务中出现了内存泄漏。问题组合为 plumatic schema + ring swagger + compojure api。在该次提交中,schema 更改为返回一个匿名函数:[链接](https://github.com/plumatic/schema/commit/2191f9e2982da74410c14686ca6c3436e802afc0) 和 ring swagger 使用 `identity` 作为调度函数 [链接](https://github.com/metosin/ring-swagger/blob/0ef9046174dec0ebd4cbf97d6cc5c6846ef11996/src/ring/swagger/coerce.clj#L178)。因此,当在一个 compojure api 调用中使用查询参数中的映射时,最终会在每个请求中将匿名函数放入 `ring.swagger.coerce/custom-matcher`(以及 `time-matcher`)方法缓存中。我们通过降级 schema 并提高迁移到 spec/malli 的优先级来解决我们的问题,但我想加一个备注,说明有些库在野外使用 `identity` 作为多方法调度函数,这似乎是一个我们应该注意的红旗。
by
我不知道这件事,ring-swagger 问题也是我造成的。很高兴在那里解决这个问题。
...