When configuring Blocking Execution and using a blocking endpoint, the request does not get properly offloaded when request input is available.
Versions: * Spring Boot 3.2.3 * Spring Framework: 6.1.4 * Java: 21
Sample Code
Here is a sample controller with a GET and a POST.
@RestController
@RequestMapping("/api")
public class DemoController {
private static final Logger LOGGER = LoggerFactory.getLogger(DemoController.class);
@GetMapping(value = "/get", produces = TEXT_PLAIN_VALUE)
public ResponseEntity<String> methodBlockingWithRequestBody() {
return testSafeBlockingThread();
}
@PostMapping(value = "/post", consumes = APPLICATION_JSON_VALUE, produces = TEXT_PLAIN_VALUE)
public ResponseEntity<String> methodBlockingWithRequestBody(@RequestBody final Map<String, String> data) {
return testSafeBlockingThread();
}
private ResponseEntity<String> testSafeBlockingThread() {
final String threadName = Thread.currentThread().getName();
if (Schedulers.isInNonBlockingThread()) {
LOGGER.error("Non-blocking thread: {}", threadName);
return ResponseEntity.internalServerError().body("Non-blocking thread: " + threadName);
}
LOGGER.info("Blocking-safe thread: {}", threadName);
return ResponseEntity.ok("Blocking-safe thread: " + threadName);
}
}
If you make a call to GET /api/get
, the response will be 200 with a thread name similar to task-1
. If you make a call to POST /api/post
with any JSON request body such at {"foo":"bar"}
the response will be a 500 with a thread name similar to reactor-http-nio-5
.
Expected Results
I would expect both blocking endpoints to be running on a blocking-safe thread.
Notes
I've attached a gzip of a simple application generated from start.spring.io which registers a WebFluxConfigurer
to configure a BlockingExecutionConfigurer
and has the above controller:
Comment From: blake-bauman
We do have a bit of a hacky workaround which seems to work functionally so developers can continue, but we hope to not have to go into production with this. The workaround is to create a RequestMappingHandlerAdapter
returned by WebFluxRegistrations
. Pseudo-code:
if (shouldOffload(handlerMethod)) {
final MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
for (int i = 0; i < methodParameters.length; i++) {
final HandlerMethodArgumentResolver resolver = findArgRequiringOffloading(methodParameters[i]);
if (resolver != null) {
// Wrap any method parameters that must be resolved in a blocking manner
methodParameters[i] = new BlockingMethodParameter(methodParameters[i], ...);
}
}
}
Then create a HandlerMethodArgumentResolver
which looks for any arg of type BlockingMethodParameter
. Pseudo-code:
@Override
public boolean supportsParameter(final MethodParameter parameter) {
return parameter instanceof BlockingMethodParameter;
}
@Override
public Mono<Object> resolveArgument(final MethodParameter param, final BindingContext bindingContext, final ServerWebExchange exchange) {
if (param instanceof BlockingMethodParameter) {
return actualArgumentResolver.resolveArgument(...)
.publishOn(offloadScheduler);
}
}
Comment From: simonbasle
Indeed, the resolving of arguments causes a thread hop to the netty thread because the Map
is decoded from the request body which is emitted by reactor-netty on the netty thread. When arguments get zipped, this causes behavior similar in spirit to a publishOn(nettyThread)
, which modifies the thread on which the method invocation will take place despite framework having correctly subscribed to the whole thing on the task-x
thread...
And testament to that, your workaround uses an explicit publishOn(offloadScheduler)
to hop back to the desired thread right before the controller method invocation 👍
We should find a way to do something equivalent directly in InvocableHandlerMethod
, somehow hinting to that class that there's a scheduler
to use when calling it from RequestMappingHandlerAdapter
.
Comment From: blake-bauman
Sounds good to me. I'm happy to try out a snapshot when one becomes available.
Comment From: snicoll
@blake-bauman, a new Spring Framework 6.1.6-SNAPSHOT
should be available shortly. Let us know if you find a problem with the fix. Thanks!
Comment From: blake-bauman
Looks good! I'm getting this now with @RequestBody
and @RequestPart
now:
Blocking-safe thread: task-2