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

我不知道这是否算得上是一个泄漏,更像是设计假设,即派发值的数量是有限的。

之前,我们没有缓存导致:none案子的派发值,这使得:none非常慢(这是多态方法长期被认为是“慢”的主要原因)。在1.8版本中,http://dev.clojure.org/jira/browse/CLJ-1429 修改了这一点,并为许多实际程序带来了实质性的性能提升。

可能存在某种中间地带,不确定。

在此场景中,使用协议是否是避免缓存溢出的更好选择?或者,也许只需要手动实现 `case` 并避免使用多方法和协议?
我的担忧,与其说是开发者错误导致的意外泄露,不如说是如果是(可能是恶意的,或是高度随机的)用户输入可以击中多方法,那么这可能会引起真正的问题。

我相信大多数Clojure程序员都很小心,如果明确表示在此进行了缓存,那么人们就会三思而后行,不再有:default 方法,或者找到其他方法来关闭多方法的输入——但并不明确。我已经在专业上使用Clojure多年了,但我并不知道:default 匹配被缓存。

例如,memoize 会导致相同类型的问题,但我认为在那种情况下更清晰(你可能说是显而易见) whereas multi-methods 对于我来说有点过于微妙。

我不知道是否有解决方案,当然,快速响应很重要,而且在几乎所有情况下,我确信它是没事的。
假设这是可能的,是否有使用 clojure.core.cache 的 LRU 或其他缓存来解决此问题的 `defmulti` 选项?(“慢”的未缓存的 multiprothod 查找是否比安全边界的缓存开销更大?)
需要注意,这种缓存发生在分发函数的输出上,而不是在多方法或分发函数的输入上。最常见的是类型判断(`type`)、类别判断(`class`)或用于从映射中提取字段的关键字。虽然这些都没有界限,但它们通常比领域更有限制。我不记得有人在我修改后的五年多时间里报告过这种情况。协议也使用缓存(每个调用点),事实上(除了ns/var表),这些都是Clojure运行时内部两个主要的状态性事物。

您可以添加许多潜在的控件,这与memoize非常类似。在后者的情况下,我们将它们推出到core.memoize/core.cache,同时保留了核心中的简单版本。我对这个问题没有明确的支持或反对意见,需要一些评估。我已经创建了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可以作为无效选择时的回退。
仅为另一案例添加到可能导致误会的事故列表中。近期有一个服务出现了内存泄漏,是由于升级某些依赖造成的。问题组合是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调用中查询参数使用一个map时,它最终会在每个请求中将匿名函数放入`ring.swagger.coerce/custom-matcher`方法缓存(以及`time-matcher`)中。我们通过降级schema并提高迁移到spec/malli的优先级来解决问题,但我想添加一点,即野外有一些库使用`identity`作为多方法分发函数,这看起来像是我们应该注意的红旗。
我没有意识到这一点,ring-swagger的问题是我的责任。我愿意解决这个问题。
...