Closed yuryfunikov closed 6 years ago
I must say this use case is very strange - why are you using SseEmitter
and Flux
? As of Spring 5, Spring MVC also supports Flux
return types does the adapting work for you properly.
Note that using reactor, scheduling work on elastic
and calling Thread.sleep
in the middle of a reactive pipeline will lead to inconsistent, unsupported behavior.
Clients disconnecting abruptly will always be an issue and your server might write to the network before knowing the client has left. This can be reproduced in a real-world environment (e.g. with network latency) and your sample might just trigger that in an artificial manner.
Could we take a step back and focus on your use case and what you're trying to achieve?
Thank you for the answer! You are right that Flux is not relevant to the issue. I have updated my example and now it does't use reactor though the problem remains - method sseEmitter() is called twice for one execution of curl
@yuryfunikov How are you running your application? In your IDE? As an executable JAR? Deployed a WAR in a container?
Hi, from IDE (Idea) or from command line using smth like java -jar target/fat-jar. I have created a sample repository with example here https://github.com/yuryfunikov/spring-sse/tree/master you can use to see the problem. Bad news is that it seems ErrorMvcAutoConfiguration is not guilty and the issue happens with or without it.
I am seeing exactly same issue in one of our projects. Thanks @yuryfunikov for providing an example code. That precisely shows my problem as well.
So it seems your sample is doing the following:
/sse
endpoint is called, an SseEmitter
is created to asynchronously write data to the response (in a non-container thread)Unlike your issue description, I don't see (with or without ErrorMvcAutoConfiguration
) your sseEmitter
endpoint being called again after the client disconnects (no /see
INFO log).
Now as far a the spec goes, we are not notified when a client goes away, so trying to get the connection and write to it (thus, getting an exception) is the only way to know a client is gone.
I'm closing this issue as we can't address that limitation in Spring Boot or Spring Framework. Feel free to reopen this issue if I've missed something.
Thanks!
Ok, i've got some time to write a test. Please take a look https://github.com/yuryfunikov/spring-sse/blob/master/src/test/java/com/example/demo/SseTest.java.
The test calls /sse endpoint again and again up to 40 times and checks that number of calls from the client to be the same as number of method calls on server. If it becomes unequal the test fails.
I still insist there's a bug of some kind that results in controller's method called again. It's called again even with POST method being used so i think it's quite critical for all who uses SseEmitter. Can you please re-open the issue.
Issue #10566 has provided a test case and has asked that this one be re-opened
Comment from @10566 :
Using Spring Boot 1.5.7-RELEASE and Spring MVC and having just one application and controller classes:
The full example with test case is here
@SpringBootApplication
public class SpringSseApplication {
public static void main(String[] args) {
SpringApplication.run(SpringSseApplication.class, args);
}
}
@RestController
public class SseController {
private static final Logger logger = LoggerFactory.getLogger(SseController.class);
@RequestMapping("/sse")
public SseEmitter sseEmitter2() {
logger.info("/sse");
SseEmitter emitter = new SseEmitter();
new Thread(()->{
for (int i = 0; i < 1000; i++) {
try {
logger.info("next: {}", i);
emitter.send("next: " + i);
} catch (IOException e) {
logger.info("IOException ");
break;
}
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
logger.info("interrupted...");
break;
}
}
emitter.complete();
}).start();
return emitter;
}
}
Controller has request handler that returns SseEmitter
object. Then i start the application and SSE data consumer for instance using curl:
curl -i -H "Accept: application/json" http://localhost:8080/sse
In case if SSE data stream is stopped by client (e.g. with ctrl+c) there's a chance (~10%) that sseEmitter()
method will be called again and you will see 'next: xxx' messages in server's console (though there's no connection to client anymore) without any errors. I.e. one will see console output like this:
2017-09-18 20:16:05.702 INFO 19416 --- [nio-8081-exec-3] com.example.springsse.SseController : /sse
2017-09-18 20:16:05.703 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 0
2017-09-18 20:16:06.703 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 1
2017-09-18 20:16:07.707 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 2
2017-09-18 20:16:08.711 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 3
2017-09-18 20:16:09.714 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 4
2017-09-18 20:16:10.718 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 5
2017-09-18 20:16:11.722 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 6
2017-09-18 20:16:12.725 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : next: 7
2017-09-18 20:16:12.727 INFO 19416 --- [ Thread-14] o.apache.coyote.http11.Http11Processor : An error occurred in processing while on a non-container thread. The connection will be closed immediately
java.io.IOException: An existing connection was forcibly closed by the remote host
at sun.nio.ch.SocketDispatcher.write0(Native Method) ~[na:1.8.0_71]
...
at java.lang.Thread.run(Thread.java:745) ~[na:1.8.0_71]
2017-09-18 20:16:12.733 INFO 19416 --- [ Thread-14] com.example.springsse.SseController : IOException
2017-09-18 20:16:12.737 ERROR 19416 --- [nio-8081-exec-4] o.a.c.c.C.[Tomcat].[localhost] : Exception Processing ErrorPage[errorCode=0, location=/error]
org.apache.catalina.connector.ClientAbortException: java.io.IOException: An existing connection was forcibly closed by the remote host
...
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.20.jar:8.5.20]
...
2017-09-18 20:16:12.739 INFO 19416 --- [nio-8081-exec-4] com.example.springsse.SseController : /sse
2017-09-18 20:16:12.740 INFO 19416 --- [ Thread-15] com.example.springsse.SseController : next: 0
2017-09-18 20:16:13.740 INFO 19416 --- [ Thread-15] com.example.springsse.SseController : next: 1
2017-09-18 20:16:14.740 INFO 19416 --- [ Thread-15] com.example.springsse.SseController : next: 2
I wrote a test that calls /sse
endpoint again and again up to 40 times and checks that number of calls from the client is the same as number of method calls on the server. If it becomes unequal the test fails.
I still think there's a bug of some kind that results in controller's method called again. It's called again even with POST method being used so i think it's quite critical for all who uses SseEmitter
. Can you please re-open the issue.
I'm not sure if the test really changes the comments in https://github.com/spring-projects/spring-boot/issues/10332#issuecomment-333475757 but I've re-opened the issue in case @bclozel has a chance to check.
Closing in favor of SPR-16058. @yuryfunikov, I've sent you a PR on your repro project to make things a bit clearer.
Thanks!
Thank you! Merged the PR to master
Bug report edit: removed reactor-core:3.0.7.RELEASE dependency since it's not important for this issue Using Spring Boot 1.5.7-RELEASE and Spring MVC and having just one application and controller classes: The full example with test case is here https://github.com/yuryfunikov/spring-sse/tree/master
Controller has request handler that returns SseEmitter object. Then i start SSE data consumer for instance using curl:
curl -i -H "Accept: application/json" http://localhost:8080/sse
In case if SSE data stream is stopped by client (e.g. with ctrl+c) there's a chance (~50%) that sseEmitter() method will be called again and you will see 'next: xxx' messages in server's console (though there's no connection to client anymore) without any errors. I.e. one will see console output like this:
The problem doesn't happen if you exclude ErrorMvcAutoConfiguration class.