Bug Description

When there is an jvm exception occur on Schedulers.parallel or other async thread pool, the connection will not close after the sever already finish the request.

Example

@GetMapping("test_exception")
public Mono<Object> handleEvent() {
    return Mono.just(true).subscribeOn(Schedulers.parallel()).map(x -> {
        throw new StackOverflowError("test");
    });
}

the server already throw exception Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler but not finish the connection Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler

Probable Cause

  • I read the code, and found when the exception is fatal error, it will direct throw the exception in thread pool without cancel subscription
  • this is where the exception throw Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler
  • then, it go to the onNextError reactor.core.publisher.Operators#onNextError(T, java.lang.Throwable, reactor.util.context.Context, org.reactivestreams.Subscription) Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler
  • just throw without cancel Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler
  • then the reactor.core.scheduler.SchedulerTask#call catch the ex, but still not call onError to cancel subscribtion Spring Connection not closed in WebFlux when JVM fatal error is raised on parallel Scheduler

Comment From: rstoyanchev

I've edited your comment to improve the formatting. You might want to check out this Mastering Markdown guide for future reference.

Also please use text instead of images where feasible, e.g. for logs and stacktraces (makes it searchable) and you can also link to GitHub source code lines.

Comment From: rstoyanchev

I can confirm the issue. It's easy to reproduce but only with both subscribeOn(Schedulers.parallel()) and the exception being a "fatal" error.

From what I can see operators are propagating the error with Exceptions.throwIfFatal(error) via Operators#onOperatorError until it eventually reaches a catch clause in HttpServerHandle which closes the connection. However when subscribeOn is used the error ends at SchedulerTask and nothing further happens in the rest of the chain.

@simonbasle what is your read on this? I don't see what we can do from a Spring Framework perspective as there are no further signals.

Comment From: simonbasle

@rstoyanchev @violetagg I don't know, I have no particular take. How would a blocking server react to something running in a new Thread() triggering an OutOfMemoryException? Does the exception ends up in the global uncaught exception handler and if so, can that help ?

Comment From: rstoyanchev

I'm afraid there isn't much we can do in case of a SO exception other than let it bubble up. If the handling happens in the Netty thread then that will be caught by Reactor Netty, but with doOnSubscribe it simply bubbles out of the Scheduler thread and all we can do is log it.

Even if you were to catch this error within your code, there isn't much you could do with a StackOverflowException. You'll need to find and address the root cause for that overflow.