4

我正在DeferredResultTomcat上试验Spring,我得到了疯狂的结果。是我做错了什么,还是 Spring 或 Tomcat 中存在一些错误?我的代码很简单。

@Controller
public class Test {
    private DeferredResult<String> deferred;

    static class DoSomethingUseful implements Runnable {
        public void run() {
            try { Thread.sleep(2000); } catch (InterruptedException e) { }
        }
    }

    @RequestMapping(value="/test/start")
    @ResponseBody
    public synchronized DeferredResult<String> start() {
        deferred = new DeferredResult<>(4000L, "timeout\n");
        deferred.onTimeout(new DoSomethingUseful());
        return deferred;
    }

    @RequestMapping(value="/test/stop")
    @ResponseBody
    public synchronized String stop() {
        deferred.setResult("stopped\n");
        return "ok\n";
    }
}

所以。该start请求创建一个DeferredResult4 秒超时。该stop请求将在DeferredResult. 如果您stop在延迟结果超时之前或之后发送,一切正常。

但是,如果您stop在超时的同时发送start,事情就会变得疯狂。我添加了一个onTimeout操作以使其易于重现,但这不是问题发生所必需的。使用 APR 连接器,它只会死锁。使用 NIO 连接器,它有时可以工作,但有时它会错误地将“超时”消息发送给stop客户端,并且永远不会回答start客户端。

要对此进行测试:

curl http://localhost/test/start & sleep 5; curl http://localhost/test/stop

我不认为我做错了什么。Spring 文档似乎说可以随时调用setResult,即使在请求已经过期之后,也可以从任何线程调用(“应用程序可以从它选择的线程产生结果”)。

使用的版本:Linux 上的 Tomcat 7.0.39,Spring 3.2.2。

4

1 回答 1

3

这是一个很好的错误发现!
只需添加有关该错误(已修复)的更多信息,以便更好地理解。

setResult() 中有一个同步块,它一直延伸到提交调度的部分。如果同时发生超时,这可能会导致死锁,因为 Tomcat 超时线程有自己的锁定,只允许一个线程进行超时或分派处理。

详细解释:

当您在请求“超时”的同时调用“停止”时,两个线程正在尝试锁定 DeferredResult 对象“延迟”。

  1. 执行“onTimeout”处理程序的线程这是 Spring 文档的摘录:

    当异步请求在 DeferredResult 设置之前超时时,从容器线程调用此 onTimeout 方法。它可以调用setResult或 setErrorResult 来恢复处理。

  2. 另一个执行“停止”服务的线程。

如果在 stop() 服务期间调用的调度处理获得了“延迟”锁,它将等待一个 tomcat 锁(比如 TomcatLock)来完成调度。
并且如果执行超时处理的其他线程已经获得了 TomcatLock,则该线程等待在“延迟”上获得锁以完成 setResult()!

所以,我们最终陷入了典型的僵局!

于 2013-05-31T17:54:40.780 回答