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

欢迎!请参阅关于页面了解更多有关该功能的信息。

+1
IO

clojure.java.shell/sh 函数的末尾具有以下结构

(with-open [stdout (.getInputStream proc)
            stderr (.getErrorStream proc)]
  (let [out (future (stream-to-enc stdout out-enc))
        err (future (stream-to-string stderr))
        exit-code (.waitFor proc)]
    {:exit exit-code :out @out :err @err}))

因此,进程的输出在两个 future 中读取,而当前线程等待进程结束(并生成退出代码)。如果在 future 的执行过程中发生异常,该异常被存储并抛出(封装在 ExecutionException 中),当 futures 被解引用时(在此情况下这是在进程结束后)。

我曾经调用 git 并使用 sh,生成的输出足够多以至于产生 OutOfMemoryError。这就是您会遇到“死锁”的地方(实际上它不是一个死锁)。

  • Git 正在将数据推入管道,而 Java 进程正在 future 中读取它,主线程正在等待进程结束
  • 读取 out 的 future 遇到 OutOfMemory 异常,该异常被存储并为 out 读取器循环终止
  • 进程之间的管道缓冲区变得满载,因为 git 正在写入,但没有人在读取
  • Git 无限期地陷入尝试写入管道的僵局中
  • Java 进程主线程无限期地等待 git 进程结束
  • 未来永远未被解引用,因此永远不会产生异常堆栈跟踪或消息

这可能在两个 future 读取过程中的任何类型的异常都会发生。另一个可能的场景是,如果指定了编码,而底层进程返回一些无法解析为该编码的字节。

这个“错误”相当令人沮丧,因为它没有错误消息,并且几乎没有关于问题的指示。在我的情况下,它在生产环境中偶尔发生,并且很难追踪。

我不知道补救办法,但可能需要对此进行某种形式的缓解。

1 答案

+1
目前我已经编写了自己的sh脚本,其中futures中的代码被包装在try-catch块中,在catch块中将输入流读取到一个非常小的缓冲区中,并以循环方式读取(缓冲区从未被使用),直到它耗尽。当发生OOM时,在栈上读取一个int可能更智能。
这个问题的一个解决方案是使用新的(Clojure 1.12中的)clojure.java.process api

    (require '[clojure.java.process :as process])
    (def result (process/exec "git" " whatever")))

如果遇到OOME,你会得到一个异常,比如

    在clojure.java.process/exec (process.clj:171)处执行错误。
    进程以退出代码=141失败

我相信141通常表示SIGPIPE,这意味着至少不会挂起。
...