-
Hello, I'm seeing some weird behavior when using a @RabbitListener on a method that returns Mono. It works great when I build it with a "regular" JVM. But it only works for acking when I build it with GraalVM into a native image. Maybe there's something that the compiler is removing because it thinks it won't be used, but it's not causing any exception. I'm not sure if it would be a problem with spring-amqp, but maybe someone might have some better understanding of what is going on. It just tries to nack (there is a log I created a demo project using Spring Initializr to isolate the problem. This is my build.gradle:
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.2'
id 'io.spring.dependency-management' version '1.1.2'
id 'org.graalvm.buildtools.native' version '0.9.23'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.projectreactor:reactor-test'
testImplementation 'org.springframework.amqp:spring-rabbit-test'
}
tasks.named('test') {
useJUnitPlatform()
}
I have a configuration class to set up the queue and the dead letter: package com.example.demo;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Queue {
public static final String QUEUE = "queue.items";
public static final String DEAD_LETTER_EXCHANGE = "exchange.deadLetter";
public static final String DEAD_LETTER_QUEUE = "queue.deadLetter";
@Bean
public org.springframework.amqp.core.Queue getQueue() {
return QueueBuilder.durable(QUEUE)
.deadLetterExchange(DEAD_LETTER_EXCHANGE)
.build();
}
@Bean
public org.springframework.amqp.core.Queue getDLQueue() {
return QueueBuilder.durable(DEAD_LETTER_QUEUE)
.build();
}
@Bean
public FanoutExchange getDLExchange() {
return ExchangeBuilder.fanoutExchange(DEAD_LETTER_EXCHANGE)
.durable(true)
.build();
}
@Bean
public Binding getDLBinding() {
return BindingBuilder.bind(getDLQueue())
.to(getDLExchange());
}
} And the application class with some comments: package com.example.demo;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.messaging.handler.annotation.Payload;
import reactor.core.publisher.Mono;
import java.util.Locale;
import static com.example.demo.Queue.QUEUE;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
//ackMode MANUAL because using Mono.
//The processing is doing nothing, just a 15s sleep to make sure that
// it is not acking right away before the mono is actually run.
//The only message that returns error is "lalala". That is the one to use
// to test the NACKING. Any other will end up ACKING.
@RabbitListener(queues = QUEUE, ackMode = "MANUAL")
public Mono<Void> test(@Payload String text) {
return Mono.just(text)
.map(t -> {
System.out.println(t);
try {
Thread.sleep(15000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (text.equals("lalala"))
throw new RuntimeException();
return t.toUpperCase(Locale.ROOT);
})
.then();
}
} To try to get some more info I enabled the TRACE log for org.springframework.amqp.rabbit. GraalVM app logs: (the exceptions are the one I'm throwing when message is lalala)
The full demo project: demo.zip edit1: edit2: |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 7 replies
-
OK. Can we start over, please? So, you claim that NACK'ed messages go to Dead Letter without any options in regular JVM, but don't in native image?
which confirms that it doesn't go to DL.
So, what should we expect from your latest zip? |
Beta Was this translation helpful? Give feedback.
-
Thank you for your attention @artembilan. I think I was at the same time verbose and also incomplete because I forgot to mention the retry section. I didn't think it would be affecting the scenario, but it seems to be. When I have nothing on my application.yml the message is read repeatedly from the queue if it causes an error. It doesn't matter the type of build. That is the default behaviour, right? But when I have this in place: spring:
rabbitmq:
listener:
simple:
retry:
enabled: true
initial-interval: 2000
max-attempts: 2
multiplier: 1 I'm telling it that after it retries 2 times it has to stop, so that is when it will send the message to the DL. And it happens for the regular build. Having 1 problematic message will make the method marked as @RabbitListener to be called twice. So, going back to actually answering what you askedAbout the log line you highlighted it is in fact different comparing both scenarios. The regular one is:
and the one from the native image is:
From what I could get from here the default value for defaultRequeueRejected is true. For the regular build I didn't set it to false (Rejecting messages (requeue=false)). Maybe it is false because I have the retry on. I don't know. But anyway it's different comparing the jvm and native. setting spring.rabbitmq.listener.simple.default-requeue-rejected to true or false didn't change the difference between build types (maybe because retry is on?). And just in case, the last version I was testing: newest-demo.zip (it seems I should have created a repo for this hehe) |
Beta Was this translation helpful? Give feedback.
-
Thank you for the report, this is indeed a bug in how Spring Boot generates reflection metadata for In the meantime, you can add the missing hints as a workaround, something like the following: static class RabbitMissingRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerTypes(TypeReference.listOf(BaseContainer.class, AmqpContainer.class, SimpleContainer.class, Retry.class, ListenerRetry.class),
(type) -> type.withConstructor(Collections.emptyList(), ExecutableMode.INVOKE)
.withMembers(MemberCategory.INVOKE_DECLARED_METHODS));
}
} And you need to import this class for processing to occur. For instance on your main class: @SpringBootApplication
@ImportRuntimeHints(RabbitMissingRuntimeHints.class)
public class MyApplication { ... } |
Beta Was this translation helpful? Give feedback.
Thank you for the report, this is indeed a bug in how Spring Boot generates reflection metadata for
ConfigurationProperties
types in case of complex class nesting. Please follow spring-projects/spring-boot#36909 for further updates.In the meantime, you can add the missing hints as a workaround, something like the following: