spring-attic / spring-native

Spring Native is now superseded by Spring Boot 3 official native support
https://docs.spring.io/spring-boot/docs/current/reference/html/native-image.html
Apache License 2.0
2.74k stars 355 forks source link

Enable `-DspringAot=true` causes `No encoder for java.lang.Boolean` MessagingException #1607

Closed kdvolder closed 2 years ago

kdvolder commented 2 years ago

I will try to produce a minimally reproducing sample, but it is hard extract this from the code where I am encountering it. When I have the sample I will attach it here.

In the mean time let me describe what's happening.

I am updating an app that uses RSocket and has a controller method like this:

    @GetMapping("/instance/{id}/env/probe")
    public Mono<Boolean> isEnvEditable(@PathVariable String id) {
        validate(id);
        final AppInstance instance = apps.getInstanceById(id);
        final SidecarClient sidecar = requesterRepo.getSidecarClient(instance.getClientId());
        return sidecar.isEnvEditable(id);
    }

Things broke in the native-compiled version of the app after upgrading to Spring Boot 2.6.7 from 2.5.12 and spring native to 0.11.4 from 0.10.6.

I am trying to collect data using the native trace agent. And I am running the app using an argument like:

java -DspringAot=true -agentlib:native-image-agent=config-output-dir=/tmp/META-INF/native-image -jar /tmp/alv-connector.jar

Strangely the env/probe endpoint implemented by the method shown above does not work and produces `No encoder for java.lang.Boolean MessagingException (full trace at end of this message). This appears to be caused somehow by the -DspringAot=true argument. When this argument is changed to -DspringAot=false then all works as it should.

Stacktrace:

org.springframework.messaging.MessagingException: No encoder for java.lang.Boolean, current value type is class java.lang.Boolean
at org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler.encodeValue(AbstractEncoderMethodReturnValueHandler.java:189) ~[spring-messaging-5.3.19.jar!/:5.3.19]
at org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler.lambda$encodeContent$1(AbstractEncoderMethodReturnValueHandler.java:153) ~[spring-messaging-5.3.19.jar!/:5.3.19]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:113) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onNext(FluxMapFuseable.java:127) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1816) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoFlatMap$FlatMapInner.onNext(MonoFlatMap.java:249) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxOnErrorResume$ResumeSubscriber.onNext(FluxOnErrorResume.java:79) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxSwitchIfEmpty$SwitchIfEmptySubscriber.onNext(FluxSwitchIfEmpty.java:74) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxHide$SuppressFuseableSubscriber.onNext(FluxHide.java:137) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.complete(MonoIgnoreThen.java:292) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onNext(MonoIgnoreThen.java:187) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.subscribeNext(MonoIgnoreThen.java:236) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoIgnoreThen$ThenIgnoreMain.onComplete(MonoIgnoreThen.java:203) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.Operators$MultiSubscriptionSubscriber.onComplete(Operators.java:2058) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.MonoIgnoreElements$IgnoreElementsSubscriber.onComplete(MonoIgnoreElements.java:89) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxMapFuseable$MapFuseableSubscriber.onComplete(FluxMapFuseable.java:150) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxOnAssembly$OnAssemblySubscriber.onComplete(FluxOnAssembly.java:549) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onComplete(FluxPeek.java:260) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.core.publisher.FluxMap$MapSubscriber.onComplete(FluxMap.java:142) ~[reactor-core-3.4.17.jar!/:3.4.17]
at reactor.netty.channel.FluxReceive.onInboundComplete(FluxReceive.java:400) ~[reactor-netty-core-1.0.18.jar!/:1.0.18]
at reactor.netty.channel.ChannelOperations.onInboundComplete(ChannelOperations.java:419) ~[reactor-netty-core-1.0.18.jar!/:1.0.18]
at reactor.netty.channel.ChannelOperations.terminate(ChannelOperations.java:473) ~[reactor-netty-core-1.0.18.jar!/:1.0.18]
at reactor.netty.http.client.HttpClientOperations.onInboundNext(HttpClientOperations.java:703) ~[reactor-netty-http-1.0.18.jar!/:1.0.18]
at reactor.netty.channel.ChannelOperationsHandler.channelRead(ChannelOperationsHandler.java:93) ~[reactor-netty-core-1.0.18.jar!/:1.0.18]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.handler.timeout.IdleStateHandler.channelRead(IdleStateHandler.java:286) ~[netty-handler-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.handler.codec.ByteToMessageDecoder.fireChannelRead(ByteToMessageDecoder.java:327) ~[netty-codec-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:299) ~[netty-codec-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:251) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800) ~[netty-transport-classes-epoll-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:487) ~[netty-transport-classes-epoll-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:385) ~[netty-transport-classes-epoll-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986) ~[netty-common-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.76.Final.jar!/:4.1.76.Final]
at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.76.Final.jar!/:4.1.76.Final]
at java.base/java.lang.Thread.run(Thread.java:829) ~[na:na]
kdvolder commented 2 years ago

Sadly still no reproducible sample. I did try to create a 'toy' version of our app but couldn't reproduce the error with it.

However there is some good news. I have found a workaround for the issue and maybe this can provide some hint as to the root cause of the problem.

In our code we have the following bean definition:

    @Bean
    RSocketRequester rSocketRequester(RSocketRequester.Builder rSocketRequesterBuilder, AppLiveViewClientProperties props, List<AppLiveViewClientController> clientSideControllers) {
        String protocal = props.isSslDisabled() ? "ws://" : "wss://";
        URI uri = URI.create(protocal+props.getHost()+":"+props.getPort());
        RSocketStrategies strategies = RSocketStrategies.builder()
                .encoders(encoders -> {
                    encoders.add(new Jackson2CborEncoder());
                    encoders.add(new Jackson2JsonEncoder()); // <-- Added: Spring native workaround
                })
                .decoders(decoders -> {
                    decoders.add(new Jackson2CborDecoder());    
                    decoders.add(new Jackson2JsonDecoder()); // <-- Added: Spring native workaround 
                })
                .metadataExtractorRegistry(r -> r.metadataToExtract(MimeTypes.INSTANCE_ID, String.class, RSocketHeaders.INSTANCE_ID))
                .metadataExtractorRegistry(r -> r.metadataToExtract(MimeTypes.ACTUATOR_PROXY_REQUEST, ActuatorProxyRequest.class, RSocketHeaders.ACTUATOR_PROXY_REQUEST))
                .routeMatcher(new PathPatternRouteMatcher())
                .build();

        SocketAcceptor responder =
                RSocketMessageHandler.responder(strategies, clientSideControllers.toArray());
        log.info("Creating RSocket connector to {}:{}", props.getHost(), props.getPort());
        log.info("Creating RSocket connector url: {}", uri);
        return rSocketRequesterBuilder
                .rsocketConnector(connector -> connector
                        .fragment(10_000_000)
                        .reconnect(Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(2)))
                        .acceptor(responder))
                .websocket(uri);
    }

The lines marked with a //Added... comment are what makes it work somehow. I am not totally sure why.

Perhaps without the -DspringAot=true we automatically get a json encoder? Or perhaps the Cbor encoder somehow forgets how to encode a 'solitary' boolean value when we add springAot flag.

sdeleuze commented 2 years ago

Worth to double check with Spring Boot 3 milestone when the support will be advanced enough to test this kind of use case. Without a reproducer, I will just close the issue on Spring Native side, thanks for your effort to try to create one.