Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebFlux Blocking controller runs on non-blocking thread when request input data present #32502

Closed
blake-bauman opened this issue Mar 20, 2024 · 5 comments
Assignees
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Milestone

Comments

@blake-bauman
Copy link

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:

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Mar 20, 2024
@snicoll snicoll added the in: web Issues in web modules (web, webmvc, webflux, websocket) label Mar 21, 2024
@blake-bauman
Copy link
Author

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);
            }
        }

@snicoll snicoll self-assigned this Apr 3, 2024
@simonbasle
Copy link
Contributor

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.

@snicoll snicoll added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Apr 3, 2024
@snicoll snicoll added this to the 6.1.x milestone Apr 3, 2024
@blake-bauman
Copy link
Author

Sounds good to me. I'm happy to try out a snapshot when one becomes available.

@simonbasle simonbasle modified the milestones: 6.1.x, 6.1.6 Apr 4, 2024
snicoll added a commit that referenced this issue Apr 4, 2024
@snicoll snicoll closed this as completed in 521cda0 Apr 4, 2024
@snicoll
Copy link
Member

snicoll commented Apr 4, 2024

@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!

@blake-bauman
Copy link
Author

Looks good! I'm getting this now with @RequestBody and @RequestPart now:

Blocking-safe thread:  task-2

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: web Issues in web modules (web, webmvc, webflux, websocket) type: bug A general bug
Projects
None yet
Development

No branches or pull requests

4 participants