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` 并且避免使用多方法(multimethod)和协议呢?
by
我的担忧不在于开发者错误导致的偶发泄漏,而实际上在于,如果允许(可能是有恶意的或高度随机的)用户输入触及到多方法,可能会造成真正的问题。

我相信大多数Clojure程序员都非常谨慎,如果明确表示出这种缓存行为的话,人们就会三思而后行,考虑不要设置`:default`方法或找到关闭多方法输入的其它方法 - 但这并不清晰。我多年来一直在专业使用Clojure,并不知道`:default`匹配被缓存。

例如,memoize也会引起类似的问题,但我认为在那种情况下(你可能会说很明显)更为清晰,而多方法则对我来说有点微妙。

我不知道是否有解决方案,当然,速度是非常重要的,绝大多数情况下,我确信它是没有问题的。
by
假设这有可能,那么将会是一个解决方案,使得defmulti选项可以使用clojure.core.cache中的LRU或其他缓存来解决问题吗?(未缓存的慢速多方法查找与安全界限的缓存开销相比会慢吗?)
by
需要注意的是,这种缓存出现在调度函数的输出上,而不是出现在多方法或调度函数的输入上。最常见的调度函数包括`type`或`class`等,或者是用于从映射中提取字段的键。尽管它们都有无限的取值范围,但通常比域更为有限。我不记得自5年前更改以来还有其他人报告过这个问题。协议也会使用缓存(每个调用的位置),实际上(除ns/var表之外),这些是Clojure运行时中两个主要的具有状态性的事物。

可以添加很多潜在的控制器来控制这一点,与memoize非常相似。在memoize的情况中,我们将其推至core.memoize/core.cache,而在core中保留了更简单的版本。我并不是在支持或反对任何一方,需要一些评估。创建了一个https://clojure.atlassian.net/browse/CLJ-2626
by
完全不奇怪,不应该有太多不同的调度值。但是有时实际的调度值并不符合开发者的预期。

我在生产环境中意外地遇到了一个内存泄漏,当使用关键词作为调度函数时。我的意图是按照第一个参数中的字段关联的值进行调度,但是由于错误地找不到关键词,将关键词作为函数处理多个方法的第二个参数作为调度值。

这里是一个简单的重现示例

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

我曾希望有
```
(defmulti foo (fn [x y]
                (:type x)))
```

请注意,我的错误代码没有导致任何功能性异常,只是由于多方法的缓存机制而产生了内存泄漏。
by
尽管它适用于调度结果,但这些结果可以从外部世界或其他程序提供作为输入,除非程序员已经确保这是安全的。我认为这与协议缓存有很大不同,因为类型与值不同,想象一个基于计数的多个方法调度,你可以为0、1、2进行特殊化,然后让:默认处理其他情况,或者是HTML表单上选择的:类型字段,:默认可以作为无效选择的回退。
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 是由我处理的。很乐意在那里修复这个问题。
...