Catching Spring Integration DSL misconfiguration exceptions? - java

Is there a way to catch DestinationResolutionException and MessageDispatchingException when using DSL? These exceptions usually indicate misconfiguration but I am not sure how I could configure my flow to catch these exceptions and apply some custom logic?
#SpringBootApplication
public class IntegrationMisconfigurationExampleApplication {
public static void main(final String[] args) {
SpringApplication.run(IntegrationMisconfigurationExampleApplication.class, args);
}
#Bean
public IntegrationFlow loggingFlow() {
return IntegrationFlows.from("input")
.<String, String>transform(String::toUpperCase)
// .nullChannel();
.get();
}
#Bean
public CommandLineRunner demo() {
return args -> {
final MessagingTemplate template = messagingTemplate();
template.convertAndSend("input", "abc");
};
}
#Bean
public MessagingTemplate messagingTemplate() {
return new MessagingTemplate();
}
}
The example above throws a DestinationResolutionException because loggingFlow.transformer#0 is not properly initialized. Is there a way to catch this exception?

Those exceptions are runtime errors. We really can't determine a misconfiguration at startup.
The way to catch runtime exception like that and do some analyzing work is with the ExpressionEvaluatingRequestHandlerAdvice, which you can add to your transform(String::toUpperCase) configuration in the second argument for endpoint configuration:
.<String, String>transform(String::toUpperCase, e-> e.advice(myExpressionEvaluatingRequestHandlerAdvice()))
See more info about this advice in the Reference Manual: https://docs.spring.io/spring-integration/docs/current/reference/html/#message-handler-advice-chain
Also you need to keep in mind that transformer is really a request-reply component with required non-null return value. Therefore you really can't configure a transform() just for one-way flow. It is going to throw an exception when there is no next channel in the flow or no replyChannel header in the message.

Related

Resilience4j Not Ignoring Exceptions

JobServiceImpl.java:
#CircuitBreaker(name = "jobsApiServiceGetAllJobs", fallbackMethod = "getAllJobsFallback")
public ResponseEntity<JobsResponse> getAllJobs() {
...
throw new ApiException();
}
public ResponseEntity<JobsResponse> getAllJobsFallback(Throwable throwable) {
log.error("Fallback method called");
}
application.properties:
resilience4j.circuitbreaker.instances.jobsApiServiceGetAllJobs.ignoreExceptions=ccp.shared.platform.exception.ApiException,ccp.shared.platform.exception.BadRequestApiException
Whenever ccp.shared.platform.exception.ApiException is thrown, the fallback method is called even though I have added it in the ignoreExceptions list in the application.properties file. I want it to not trigger the fallback method when ApiException is thrown. I have tried similar questions on stack overflow and those does not work for me. Am I doing anything wrong?
Seems that the property is defined in the docs but actually, it does not work. There is a pull request for that -> CircuitBreakerConfigurationProperties has no ignoreException property.
You can use the common Spring+java configuration to achieve that in the meantime:
#Bean
CircuitBreaker reportingApiCircuitBreaker(CircuitBreakerRegistry registry) {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.ignoreExceptions(ApiException.class, BadRequestApiException.class)
.build();
return registry.circuitBreaker("yourCircuitBreakername", config);
}

Capturing Errors on Spring Integration DSL

We have a Spring Integration DSL pipeline connected to a GCP Pubsub and things "work": The data is received and processed as defined in the pipeline, using a collection of Function implementations and .handle().
The problem we have (and why I used "work" in quotes) is that, in some handlers, when some of the data isn't found in the companion database, we raise IllegalStateException, which forces the data to be reprocessed (along the way, another service may complete the companion database and then function will now work). This exception is never shown anywhere.
We tried to capture the content of errorHandler, but we really can't find the proper way of doing it programmatically (no XML).
Our Functions have something like this:
Record record = recordRepository.findById(incomingData).orElseThrow(() -> new IllegalStateException("Missing information: " + incomingData));
This IllegalStateException is the one that is not appearing anywhere in the logs.
Also, maybe it's worth mentioning that we have our channels defined as
#Bean
public DirectChannel cardInputChannel() {
return new DirectChannel();
}
#Bean
public PubSubInboundChannelAdapter cardChannelAdapter(
#Qualifier("cardInputChannel") MessageChannel inputChannel,
PubSubTemplate pubSubTemplate) {
PubSubInboundChannelAdapter adapter = new PubSubInboundChannelAdapter(pubSubTemplate, SUBSCRIPTION_NAME);
adapter.setOutputChannel(inputChannel);
adapter.setAckMode(AckMode.AUTO);
adapter.setPayloadType(CardDto.class);
return adapter;
}
I am not familiar with the adapter, but I just looked at the code and it looks like they just nack the message and don't log anything.
You can add an Advice to the handler's endpoint to capture and log the exception
.handle(..., e -> e.advice(exceptionLoggingAdvice)
#Bean
public MethodInterceptor exceptionLoggingAdvice() {
return invocation -> {
try {
return invocation.proceed();
}
catch (Exception thrown) {
// log it
throw thrown;
}
}
}
EDIT
#SpringBootApplication
public class So57224614Application {
public static void main(String[] args) {
SpringApplication.run(So57224614Application.class, args);
}
#Bean
public IntegrationFlow flow(MethodInterceptor myAdvice) {
return IntegrationFlows.from(() -> "foo", endpoint -> endpoint.poller(Pollers.fixedDelay(5000)))
.handle("crasher", "crash", endpoint -> endpoint.advice(myAdvice))
.get();
}
#Bean
public MethodInterceptor myAdvice() {
return invocation -> {
try {
return invocation.proceed();
}
catch (Exception e) {
System.out.println("Failed with " + e.getMessage());
throw e;
}
};
}
}
#Component
class Crasher {
public void crash(Message<?> msg) {
throw new RuntimeException("test");
}
}
and
Failed with nested exception is java.lang.RuntimeException: test

Spring cloud stream kafka pause/resume binders

We are using spring cloude stream 2.0 & Kafka as a message broker.
We've implemented a circuit breaker which stops the Application context, for cases where the target system (DB or 3rd party API) is unavilable, as suggested here: Stop Spring Cloud Stream #StreamListener from listening when target system is down
Now in spring cloud stream 2.0 there is a way to manage the lifecycle of binder using actuator: Binding visualization and control
Is it possible to control the binder lifecycle from the code, means in case target server is down, to pause the binder, and when it's up, to resume?
Sorry, I misread your question.
You can auto wire the BindingsEndpoint but, unfortunately, its State enum is private so you can't call changeState() programmatically.
I have opened an issue for this.
EDIT
You can do it with reflection, but it's a bit ugly...
#SpringBootApplication
#EnableBinding(Sink.class)
public class So53476384Application {
public static void main(String[] args) {
SpringApplication.run(So53476384Application.class, args);
}
#Autowired
BindingsEndpoint binding;
#Bean
public ApplicationRunner runner() {
return args -> {
Class<?> clazz = ClassUtils.forName("org.springframework.cloud.stream.endpoint.BindingsEndpoint$State",
So53476384Application.class.getClassLoader());
ReflectionUtils.doWithMethods(BindingsEndpoint.class, method -> {
try {
method.invoke(this.binding, "input", clazz.getEnumConstants()[2]); // PAUSE
}
catch (InvocationTargetException e) {
e.printStackTrace();
}
}, method -> method.getName().equals("changeState"));
};
}
#StreamListener(Sink.INPUT)
public void listen(String in) {
}
}

How to create Processor with Transaction and DLQ with Rabbit binding?

I'm just starting to learn Spring Cloud Streams and Dataflow and I want to know one of important use cases for me. I created example processor Multiplier which takes message and resends it 5 times to output.
#EnableBinding(Processor.class)
public class MultiplierProcessor {
#Autowired
private Source source;
private int repeats = 5;
#Transactional
#StreamListener(Processor.INPUT)
public void handle(String payload) {
for (int i = 0; i < repeats; i++) {
if(i == 4) {
throw new RuntimeException("EXCEPTION");
}
source.output().send(new GenericMessage<>(payload));
}
}
}
What you can see is that before 5th sending this processor crashes. Why? Because it can (programs throw exceptions). In this case I wanted to practice fault prevention on Spring Cloud Stream.
What I would like to achieve is to have input message backed in DLQ and 4 messages that were send before to be reverted and not consumed by next operand (just like in normal JMS transaction). I tried already to define following properties in my processor project but without success.
spring.cloud.stream.bindings.output.producer.autoBindDlq=true
spring.cloud.stream.bindings.output.producer.republishToDlq=true
spring.cloud.stream.bindings.output.producer.transacted=true
spring.cloud.stream.bindings.input.consumer.autoBindDlq=true
Could you tell me if it possible and also what am I doing wrong? I would be overwhelmingly thankful for some examples.
You have several issues with your configuration:
missing .rabbit in the rabbit-specific properties)
you need a group name and durable subscription to use autoBindDlq
autoBindDlq doesn't apply on the output side
The consumer has to be transacted so that the producer sends are performed in the same transaction.
I just tested this with 1.0.2.RELEASE:
spring.cloud.stream.bindings.output.destination=so8400out
spring.cloud.stream.rabbit.bindings.output.producer.transacted=true
spring.cloud.stream.bindings.input.destination=so8400in
spring.cloud.stream.bindings.input.group=so8400
spring.cloud.stream.rabbit.bindings.input.consumer.durableSubscription=true
spring.cloud.stream.rabbit.bindings.input.consumer.autoBindDlq=true
spring.cloud.stream.rabbit.bindings.input.consumer.transacted=true
and it worked as expected.
EDIT
Actually, no, the published messages were not rolled back. Investigating...
EDIT2
OK; it does work, but you can't use republishToDlq - because when that is enabled, the binder publishes the failed message to the DLQ and the transaction is committed.
When that is false, the exception is thrown to the container, the transaction is rolled back, and RabbitMQ moves the failed message to the DLQ.
Note, however, that retry is enabled by default (3 attempts) so, if your processor succeeds during retry, you will get duplicates in your output.
For this to work as you want, you need to disable retry by setting the max attempts to 1 (and don't use republishToDlq).
EDIT3
OK, if you want more control over the publishing of the errors, this will work, when the fix for this JIRA is applied to Spring AMQP...
#SpringBootApplication
#EnableBinding({ Processor.class, So39018400Application.Errors.class })
public class So39018400Application {
public static void main(String[] args) {
SpringApplication.run(So39018400Application.class, args);
}
#Bean
public Foo foo() {
return new Foo();
}
public interface Errors {
#Output("errors")
MessageChannel errorChannel();
}
private static class Foo {
#Autowired
Source source;
#Autowired
Errors errors;
#StreamListener(Processor.INPUT)
public void handle (Message<byte[]> in) {
try {
source.output().send(new GenericMessage<>("foo"));
source.output().send(new GenericMessage<>("foo"));
throw new RuntimeException("foo");
}
catch (RuntimeException e) {
errors.errorChannel().send(MessageBuilder.fromMessage(in)
.setHeader("foo", "bar") // add whatever you want, stack trace etc.
.build());
throw e;
}
}
}
}
with properties:
spring.cloud.stream.bindings.output.destination=so8400out
spring.cloud.stream.bindings.errors.destination=so8400errors
spring.cloud.stream.rabbit.bindings.errors.producer.transacted=false
spring.cloud.stream.rabbit.bindings.output.producer.transacted=true
spring.cloud.stream.bindings.input.destination=so8400in
spring.cloud.stream.bindings.input.group=so8400
spring.cloud.stream.rabbit.bindings.input.consumer.transacted=true
spring.cloud.stream.rabbit.bindings.input.consumer.requeue-rejected=false
spring.cloud.stream.bindings.input.consumer.max-attempts=1

Spring annotation #Retryable - how to set an interceptor

I am using a #Retryable annotation on a method in a #Serviceclass
#Service
#EnableRetry
public class PushService {
#Retryable(maxAttempts=5)
public Result pushIt(myMessage messageIn) {
...
}
}
and it works like a charme: I am getting a message directly from RabbitMQ, it is not acknowledged until either there is no error, or the number of attempts reach 5, and at that time the messages goes straight to the DLQ, right as I wanted.
My only problem is that I need to set the maxAttempts dynamically, from a property file. The solution should be setting an interceptor, but the only fact of having one causes an error, for example when I have :
#Service
#EnableRetry
public class PushService {
#Retryable(interceptor="myInterceptor")
public Result pushIt(myMessage messageIn) {
...
}
}
where myInterceptor is defined as :
#Bean
public StatefulRetryOperationsInterceptor myInterceptor() {
return RetryInterceptorBuilder.stateful().maxAttempts(5).build();
}
I get an infinite loop with the following exception:
2015-04-08 07:12:10,970 GMT [SimpleAsyncTaskExecutor-1] (ConditionalRejectingErrorHandler.java:67) WARN listener.ConditionalRejectingErrorHandler: Execution of Rabbit message listener failed.
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException: Listener threw exception
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.wrapToListenerExecutionFailedExceptionIfNeeded(AbstractMessageListenerContainer.java:864)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:802)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:690)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$001(SimpleMessageListenerContainer.java:82)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$1.invokeListener(SimpleMessageListenerContainer.java:167)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.invokeListener(SimpleMessageListenerContainer.java:1241)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.executeListener(AbstractMessageListenerContainer.java:660)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.doReceiveAndExecute(SimpleMessageListenerContainer.java:1005)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.receiveAndExecute(SimpleMessageListenerContainer.java:989)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer.access$700(SimpleMessageListenerContainer.java:82)
at org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer$AsyncMessageProcessingConsumer.run(SimpleMessageListenerContainer.java:1103)
at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.ArrayIndexOutOfBoundsException: 1
at org.springframework.amqp.rabbit.config.StatefulRetryOperationsInterceptorFactoryBean$3.getKey(StatefulRetryOperationsInterceptorFactoryBean.java:103)
at org.springframework.retry.interceptor.StatefulRetryOperationsInterceptor.invoke(StatefulRetryOperationsInterceptor.java:132)
at org.springframework.retry.annotation.AnnotationAwareRetryOperationsInterceptor.invoke(AnnotationAwareRetryOperationsInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:653)
at com.acme.push.service.PushService$$EnhancerBySpringCGLIB$$9d503bc1.pushMessage(<generated>)
at com.acme.push.receiver.PushListener.onMessage(PushListener.java:42)
at org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:799)
... 10 more
I am pretty sure I am keeping it too simple, but I just have no clues on what could cause this error and how to solve it, anybody has an idea of what is going on ?
The purpose of org.springframework.amqp.rabbit.config.RetryInterceptorBuilder is not to be used using the annotation #Retryable.
Its purpose is to be used with the advice chain of the SimpleRabbitListenerContainerFactory class.
See the reference documentation receiving-messages and he SimpleMessageListenerContainer#invokeListener signature:
#Override
protected void invokeListener(Channel channel, Message message) throws Exception {
proxy.invokeListener(channel, message);
}
You annotation will be useless as soon as you configure the advice chain with the appropriate RetryInterceptor.
I finally managed to obtain the needed flexibility without using the #Retrayable annotation.
I created a RetryAdvice with my parameters for delay and maximum number of attempts:
#Bean
public MethodInterceptor retryAdvice() {
FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
backOffPolicy.setBackOffPeriod(delay);
return RetryInterceptorBuilder.stateless().backOffPolicy(backOffPolicy)
.maxAttempts(maxAttempts).build();
}
and I inserted the Advice in the adviceChain of the ListenerContainer
#Bean
public SimpleMessageListenerContainer replyListenerContainer() {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(pushConnectionFactory());
container.setQueues(pushQueue());
container.setMessageListener(pushListener());
Advice[] adviceChain = new Advice[] { retryAdvice() };
container.setAdviceChain(adviceChain);
return container;
}
In this way, whenever my Listener will be throwing
throw new AmqpRejectAndDontRequeueException(cause);
this will cause the container to retry the indicated number of times with the desired delay, after which the exception will be propagated and the message will be delivered in the DLQ

Categories

Resources