Guava SimpleTimeLimiter 不会从 Process.getInputStream 超时 BufferedReader.readLine

Guava SimpleTimeLimiter does not timeout BufferedReader.readLine from Process.getInputStream

问题

我正在尝试围绕 BufferedReader.readLine() 调用设置某种超时。 BufferedReader 是从 java.lang.Process.getInputStream() 创建的。到目前为止我发现的最有前途的事情是 How do you set a timeout on BufferedReader and PrintWriter in Java 1.4? 看看 Guava 的 SimpleTimeLimiter 发现他们似乎解决了这个问题,就像我自己做的那样:通过使用 Java 7 的 Executors/Callables/Futures。然而,这似乎不起作用,因为在我的 Unix 环境中根本不会发生超时。至少不在可接受的公差范围内,可能永远不会。

代码

@Override
@Transactional
public Process call()
{
    java.lang.Process systemProcess = null;
    try
    {
        ProcessCallable.LOGGER.debug("executing command: {} ||| timeout: {} {}", this.process.getCommand(), this.getTimeout(), this.getTimeoutUnit());
        final String[] args = CommandLineUtils.translateCommandline(this.process.getCommand());
        final ProcessBuilder processBuilder = new ProcessBuilder(args);
        processBuilder.redirectErrorStream(true);
        systemProcess = processBuilder.start();
        final int pid = this.processService.getPid(systemProcess);
        try (final BufferedReader reader = new BufferedReader(new InputStreamReader(systemProcess.getInputStream()));
            final OutputStream os = systemProcess.getOutputStream())
        {
            ProcessCallable.LOGGER.debug("PID: {}", pid);
            String line = this.timeLimiter.callWithTimeout(reader::readLine, this.getTimeout(), this.getTimeoutUnit(), true);
            while (line != null)
            {
                ProcessCallable.LOGGER.debug("line: \"{}\"", line);
                line = this.timeLimiter.callWithTimeout(reader::readLine, this.getTimeout(), this.getTimeoutUnit(), true);
            }
        }
        final int exitCode = systemProcess.waitFor();
        ProcessCallable.LOGGER.debug("exit code for PID: {} = {}", pid, exitCode);
    }
    catch (final Exception ex)
    {
        ProcessCallable.LOGGER.error("error while executing command: " + this.process.getCommand(), ex);
        this.processService.killTree(systemProcess);
    }

    ...
}

日志

[2018-05-06 07:06:32] [DEBUG] [?.process.ProcessCallable] [processRunner1] executing command: /etc/init.d/starbound update ||| timeout: 1 MINUTES
[2018-05-06 07:06:32] [DEBUG] [?.service.ProcessService] [processRunner1] getting PID of java.lang.UNIXProcess
[2018-05-06 07:06:32] [DEBUG] [?.process.ProcessCallable] [processRunner1] PID: 15258
[2018-05-06 07:06:32] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "Updating Starbound Daemon"
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "Redirecting stderr to '/home/steam/logs/stderr.txt'"
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "Looks like steam didn't shutdown cleanly, scheduling immediate update check"
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "[  0%] Checking for available updates..."
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "[----] Verifying installation..."
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "Steam Console Client (c) Valve Corporation"
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "-- type 'quit' to exit --"
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: "Loading Steam API...OK."
[2018-05-06 07:06:34] [DEBUG] [?.process.ProcessCallable] [processRunner1] line: ""
[2018-05-06 07:11:22] [ERROR] [?.process.ProcessCallable] [processRunner1] error while executing command: /etc/init.d/starbound update
com.google.common.util.concurrent.UncheckedTimeoutException: java.util.concurrent.TimeoutException
        at com.google.common.util.concurrent.SimpleTimeLimiter.callWithTimeout(SimpleTimeLimiter.java:143)
        at ?.process.ProcessCallable.call(ProcessCallable.java:84)
        at ?.process.ProcessCallable.call(ProcessCallable.java:32)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:333)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:190)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
        at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:99)
        at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
        at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
        at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
        at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:213)
        at com.sun.proxy.$Proxy544.call(Unknown Source)
        at org.springframework.security.concurrent.DelegatingSecurityContextCallable.call(DelegatingSecurityContextCallable.java:86)
        at java.util.concurrent.FutureTask.run(FutureTask.java:266)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at java.lang.Thread.run(Thread.java:745)
Caused by: java.util.concurrent.TimeoutException
        at java.util.concurrent.FutureTask.get(FutureTask.java:205)
        at com.google.common.util.concurrent.SimpleTimeLimiter.callWithTimeout(SimpleTimeLimiter.java:130)
    ... 20 more

我实际上确实需要每行超时而不是每个进程。总体执行时间差异很大,我试图通过偶尔要求至少一些输出来确定进程是否已冻结。从日志中可以看出,在我最终决定从控制台终止进程之前,我已经等待 Java/SimpleTimeLimiter 大约 5 分钟(5 * 超时)来检测超时。终止进程后, TimeoutException 实际上被抛出。我很难理解为什么 future.get(timeoutDuration, timeoutUnit) 无法正确超时阻塞 BufferedReader.readLine() 调用。我很确定我过去已经成功地使用 future.get() 超时和 Input-/OutputStreams(在工作中,我什至不知道 TimeLimiter)。那么这里的问题是什么?是基础 Process?

这不起作用,因为 Guava 的 SimpletimeLimiter 只中断被调用的方法,参见:

https://github.com/google/guava/blob/master/guava/src/com/google/common/util/concurrent/SimpleTimeLimiter.java

在这种情况下,Guava 代码将尝试中断 Reader.readLine() 但 java IO 方法不支持中断。因此,您可以尝试尽可能多地中断它,该代码只是不检查线程的 isInterrupted 布尔标志,因此 Guava 调用无效。您可能想查看 Java 的 NIO(非阻塞 IO)。或者,不使用 Reader.readLine(),而是逐个字符读取并在读取之前调用 isReady()(永远不会遇到您正在调用 read() 但没有任何内容可读的情况),而且,检查您的线程是否已被中断(并使用 Guava,为什么不呢)。