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 被解析(在本例中,这是在进程结束后)。

我遇到过一种情况,在使用 sh 调用 git 时,生成的输出足够大,以至于产生了 OutOfMemoryError。这就是你得到“死锁”的地方(技术上并不是)。

  • 当 git 把数据塞入管道时,Java 进程在未来的某个时刻读取它,主线程等待进程结束。
  • 读取 out 的未来遇到 OutOfMemory 异常,它被存储,并且 out 读取循环终止
  • 进程之间的管道缓冲区填满了,因为 git 正在写入,而没有人再读取了
  • Git 试图无限期地向管道写入而停滞不前
  • Java 进程的主线程无限期地等待 git 进程结束
  • 未来从未被解析,因此从未产生异常堆栈跟踪或消息

这种情况可以在其中一个 future 的读取过程中,遇到任何类型的异常时发生。另一个实际的场景可能是,如果你指定了一个编码,而底层进程返回了一些不能在那个编码中解析的字节。

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

我不知道解决方案是什么,但为这种状况做些缓解可能是个好主意。

1 个答案

+1
目前我已自行编写了我的sh脚本,其中Future中的代码被包含在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,因此它至少不会挂起。
...