Skip to content

RSocket components fail to load when using Webflux and Spring lazy initialization. #17964

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

Closed
ccellist opened this issue Aug 26, 2019 · 6 comments
Assignees
Labels
status: declined A suggestion or change that we don't feel we should currently apply

Comments

@ccellist
Copy link

Version: Spring Boot 2.2.0.M5
Module: spring-boot-starter-rsocket

When enabling lazy initialization for spring beans (spring.main.lazy-initialization=true) rSocket components fail to load with the following exception (stack trace abbreviated):

Caused by: java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-2
	at reactor.core.publisher.BlockingSingleSubscriber.blockingGet(BlockingSingleSubscriber.java:77) ~[reactor-core-3.3.0.BUILD-20190823.142502-144.jar:3.3.0.BUILD-SNAPSHOT]
	at reactor.core.publisher.Mono.block(Mono.java:1541) ~[reactor-core-3.3.0.BUILD-20190823.142502-144.jar:3.3.0.BUILD-SNAPSHOT]
	at com.arayastudio.quickchat.config.RSocketConfig.rSocket(RSocketConfig.java:30) ~[classes/:na]
	at com.arayastudio.quickchat.config.RSocketConfig$$EnhancerBySpringCGLIB$$13ab571d.CGLIB$rSocket$0(<generated>) ~[classes/:na]
	at com.arayastudio.quickchat.config.RSocketConfig$$EnhancerBySpringCGLIB$$13ab571d$$FastClassBySpringCGLIB$$5e97efe0.invoke(<generated>) ~[classes/:na]
	at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244) ~[spring-core-5.2.0.BUILD-20190823.152058-14.jar:5.2.0.BUILD-SNAPSHOT]

This happens specifically when spring-boot-starter-webflux is also a dependency. What's happening is Spring attempts to load the rSocket components inside of a non-blocking thread when someone hits the app using a webflux-enabled controller. This triggers Schedulers.isInNonBlockingThread() to return true which throws the exception. Opening this issue at the request of Andy Wilkinson (@wilkinsona) who indicated RSocket components need to be marked as never lazy.

Sample project can be found at https://github.com/ccellist/quickchat-rsocket. Build and run both client and server apps, then try hitting the client with curl -XPOST -H 'Accept: application/json' -H 'Content-Type: application/json' http://localhost:8081/chat -d 'Hello' to trigger the exception.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Aug 26, 2019
@philwebb
Copy link
Member

See also #16615

@philwebb philwebb added type: bug A general bug and removed status: waiting-for-triage An issue we've not yet triaged labels Aug 26, 2019
@philwebb philwebb added this to the 2.2.x milestone Aug 26, 2019
@wilkinsona
Copy link
Member

Thanks for opening this, @ccellist. Much appreciated.

@ccellist
Copy link
Author

Happy to help @wilkinsona.

@shpall
Copy link

shpall commented Aug 30, 2019

Hello.

In my case i need to create new connections in runtime to some new hosts over RSocket. My solution was to create bean of type Mono<RSocket> (with cache()) instead of RSocket. Maybe this information can help somebody.

@wilkinsona maybe, if applicable, we can use reactive bean?

@wilkinsona wilkinsona self-assigned this Sep 30, 2019
@wilkinsona
Copy link
Member

wilkinsona commented Sep 30, 2019

In this particular case the bean that cannot be lazy is defined in user configuration. Here's the definition from the sample (updated to align with removal of MIME type from MetaDataExtractor):

@Bean
public RSocket rSocket() {
    return RSocketFactory
        .connect()
        .mimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE)
        .frameDecoder(PayloadDecoder.ZERO_COPY)
        .transport(TcpClientTransport.create(7000))
        .start()
        .block();
}

The problem can then be avoided by adding @Lazy(false) to the definition:

@Bean
@Lazy(false)
public RSocket rSocket() {
…

It may still make sense to exclude any io.rsocket.RSocket bean from being lazy as it's almost certainly going to have been created using an RSocketFactory and require a call to block().

@bclozel bclozel added status: declined A suggestion or change that we don't feel we should currently apply and removed type: bug A general bug labels Sep 30, 2019
@bclozel bclozel removed this from the 2.2.x milestone Sep 30, 2019
@bclozel
Copy link
Member

bclozel commented Sep 30, 2019

After chatting with @wilkinsona , we've decided not to change the current behavior.

Since 2.2.0.M5 Spring Boot improve its RSocket support, so your application could be improved with a few changes. You could delete the RSocketConfig entirely, and create a requester like this:

@Component
public class YourService {

	private final Mono<RSocketRequester> requesterMono;

	public YourService(MyCustomProperties properties, RSocketRequester.Builder builder) {
		this.requesterMono = builder
				.dataMimeType(MediaType.APPLICATION_CBOR)
				.connectTcp(properties.getHost(), properties.getPort()).retry(5).cache();
	}

	public Mono<User> findUser(String login) {
		return this.requesterMono.flatMap(req ->
				req.route("find.user.{login}", login)
						.retrieveMono(User.class));
	}

This is creating the connection lazily and you can also add retry/error handling/etc as reactor operators.

In general, creating an RSocket as a bean might not be a good solution since you can't really handle errors/retries or even properly close the connection as part of the application lifecycle.

Right now you can write a wide variety of RSocket applications with Spring Boot without creating such beans:

  • a client requester can be created and used as explained above
  • an RSocket standalone server is managed by Spring Boot and creating the RSocket connection for you as part of the server lifecycle
  • a requester that talks to a client can be injected by Spring Framework in a controller handler as a method parameter

I'm closing this issue for now; we might reopen this issue in the future if we see that best practices or Spring Boot support evolve in that matter.

Thanks!

@bclozel bclozel closed this as completed Sep 30, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: declined A suggestion or change that we don't feel we should currently apply
Projects
None yet
Development

No branches or pull requests

6 participants