我在Tomcat上尝试使用Spring的DeferredResult
,我得到了疯狂的结果。是我做错了,还是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
请求创建DeferredResult
,超时为4秒。 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。
答案 0 :(得分:3)
这是一个很好的bug发现!
只需添加有关错误的更多信息(that got fixed),以便更好地理解。
setResult()中有一个synchronized块,它扩展到提交调度的部分。如果由于Tomcat超时线程有自己的锁定只允许一个线程执行超时或分派处理,则如果同时发生超时,则可能导致死锁。
详细说明:
当你在请求“超时”的同时调用“stop”时,两个线程试图锁定DeferredResult对象'deferred'。
执行“onTimeout”处理程序的线程 以下是Spring doc的摘录:
当异步请求在设置DeferredResult之前超时时,从容器线程调用此onTimeout方法。它可以调用 setResult 或setErrorResult来恢复处理。
执行“停止”服务的另一个线程。
如果在stop()服务期间调用的调度处理获得'deferred'锁,它将等待tomcat锁(比如TomcatLock)完成调度。
如果执行超时处理的其他线程已经获取了TomcatLock,那么该线程等待获取'deferred'的锁来完成setResult()!
所以,我们最终处于经典的僵局状态!