Spring Integration - JdbcPollingChannelAdapter commit instead of rollback on handled Exceptions - spring

I am using Spring 4.1.x APIs, Spring Integration 4.1.x APIs and Spring Integration Java DSL 1.0.x APIs for an EIP flow where we consume messages from an Oracle database table using a JdbcPollingChannelAdpater as the entry point into the flow.
Even though we have an ErrorHandler configured on the JdbcPollingChannelAdapter's Poller, we are seeing that a session's Transaction is still rolled back and not committed when a RuntimeException is thrown and correctly handled by the ErrorHandler.
After reading through this thread: Spring Transactions - Prevent rollback after unchecked exceptions (RuntimeException), I get the feeling that it is not possible to prevent a rollback and instead force a commit. Is this correct? And, if there is a way, what is the cleanest way to force a commit instead of a rollback when an error is safely handled?
Current Configuration:
IntegrationConfig.java:
#Bean
public MessageSource<Object> jdbcMessageSource() {
JdbcPollingChannelAdapter adapter = new JdbcPollingChannelAdapter(
dataSource,
"select * from SERVICE_TABLE where rownum <= 10 for update skip locked");
adapter.setUpdateSql("delete from SERVICE_TABLE where SERVICE_MESSAGE_ID in (:id)");
adapter.setRowMapper(serviceMessageRowMapper);
adapter.setMaxRowsPerPoll(1);
adapter.setUpdatePerRow(true);
return adapter;
}
#SuppressWarnings("unchecked")
#Bean
public IntegrationFlow inFlow() {
return IntegrationFlows
.from(jdbcMessageSource(),
c -> {
c.poller(Pollers.fixedRate(100)
.maxMessagesPerPoll(10)
.transactional(transactionManager)
.errorHandler(errorHandler));
})
.channel(inProcessCh()).get();
}
ErrorHandler.java
#Component
public class ErrorHandler implements org.springframework.util.ErrorHandler {
#Autowired
private PlatformTransactionManager transactionManager;
private static final Logger logger = LogManager.getLogger();
#Override
public void handleError(Throwable t) {
logger.trace("handling error:{}", t.getMessage(), t);
// handle error code here...
// we want to force commit the transaction here?
TransactionStatus txStatus = transactionManager.getTransaction(null);
transactionManager.commit(txStatus);
}
}
--- EDITED to include ExpressionEvaluatingRequestHandlerAdvice Bean ---
#Bean
public Advice expressionEvaluatingRequestHandlerAdvice() {
ExpressionEvaluatingRequestHandlerAdvice expressionEvaluatingRequestHandlerAdvice = new ExpressionEvaluatingRequestHandlerAdvice();
expressionEvaluatingRequestHandlerAdvice.setTrapException(true);
expressionEvaluatingRequestHandlerAdvice.setOnSuccessExpression("payload");
expressionEvaluatingRequestHandlerAdvice
.setOnFailureExpression("payload");
expressionEvaluatingRequestHandlerAdvice.setFailureChannel(errorCh());
return expressionEvaluatingRequestHandlerAdvice;
}
--- EDITED to show Dummy Test Message handler ---
.handle(Message.class,
(m, h) -> {
boolean forceTestError = m.getHeaders().get("forceTestError");
if (forceTestError) {
logger.trace("simulated forced TestException");
TestException testException = new TestException(
"forced test exception");
throw testException;
}
logger.trace("simulated successful process");
return m;
}, e-> e.advice(expressionEvaluatingRequestHandlerAdvice())
--- EDITED to show ExecutorChannelInterceptor method ---
#Bean
public IntegrationFlow inFlow() {
return IntegrationFlows
.from(jdbcMessageSource(), c -> {
c.poller(Pollers.fixedRate(100).maxMessagesPerPoll(10)
.transactional(transactionManager));
})
.enrichHeaders(h -> h.header("errorChannel", errorCh(), true))
.channel(
MessageChannels.executor("testSyncTaskExecutor",
syncTaskExecutor()).interceptor(
testExecutorChannelInterceptor()))
.handle(Message.class, (m, h) -> {
boolean forceTestError = m.getHeaders().get("forceTestError");
if (forceTestError) {
logger.trace("simulated forced TestException");
TestException testException = new TestException(
"forced test exception");
throw testException;
}
logger.trace("simulated successful process");
}).channel("nullChannel").get();
}

It won't work just because your ErrorHandler works already after the finish of TX.
Here is a couple lines of source code (AbstractPollingEndpoint.Poller):
#Override
public void run() {
taskExecutor.execute(new Runnable() {
#Override
public void run() {
.............
try {
if (!pollingTask.call()) {
break;
}
count++;
}
catch (Exception e) {
....
}
}
}
});
}
Where:
The ErrorHandler is applied for the taskExecutor (SyncTaskExecutor) by default.
TransactionInterceptor being as Aspect is applied for the Proxy around that pollingTask.
Therefore TX is done around the pollingTask.call() and goes out. And only after that your ErrorHandler starts to work inside taskExecutor.execute().
To fix your issue, you need to figure out which downstream flow part isn't so critical for TX rallback and make there some try...catch or use ExpressionEvaluatingRequestHandlerAdvice to "burke" that RuntimeException.
But as you have noticed by my reasoning that must be done within TX.

Related

Spring JPA transaction partially retry

I am trying to use Spring retry for my Spring web application (with JPA, hibernate).
Currently, I have a long transaction chain (marked functions as #Transactional) in my application, intuitively:
start transction -> A() -> B() -> C() -> D() - E() -> commit/rollback
now I want to have D() to be retried if any concurrency failure occurred (marked D() with #retryable), but remain A(), B(), C() in current states
I failed, the function D() is not retried at all and just throwed a concurrency failure error (eg. ObjectOptimisticLockingFailureException)
For me, if I want to do such things in database, I have to make a new transaction try catch block with a cursor while loop to handle retries. I wonder is there a simple way I can handle this "partial" transaction retry in Spring?
An example code would be:
#RestController
public DimensionController()
{
...
#Autowired
private TblDimensionService dimensionService;
...
#PutMapping(...)
public ResponseEntity<TblDimensionDTO> update(#Valid #RequestBody TblDimensionDTO dimensionDTO)
{
...
dimensionService.update(dimensionDTO);
...
}
}
#Transactional //transaction in service level
#Service
public TblDimensionService()
{
...
#Autowired
private RetryService retryService;
...
public TblDimensionDTO update(TblDimensionDTO dimensionDTO) throws InterruptedException
{
if (dimensionDTO.getId() == null)
{
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "not found");
}
TblDimension dimension = findByIdOrElseThrow(dimensionDTO.getId()); //call another function to retrieve entity by id with JPA repository
dimension = retryService.updateEntity(dimension, dimensionDTO);
return tblDimensionMapper.toDto(dimension);
}
...
}
#Transactional //transaction in service level
#Service
public RetryService()
{
...
#Autowired
private TblDimensionRepository dimensionRepository;
...
//I want to only retry this part
//but this retry does not work
#Retryable(value = {ConcurrencyFailureException.class})
public TblDimension updateEntity(TblDimension dimension, TblDimensionDTO dimensionDTO) throws InterruptedException
{
Thread.sleep(3000);
dimension.setHeight(dimension.getHeight() + 1);
Thread.sleep(3000);
return dimensionRepository.save(dimension);
}
...
}

Kafka Consumer with Circuit Breaker, Retry Patterns using Resilience4j

I need some help in understanding how I can come up with a solution using Spring boot, Kafka, Resilence4J to achieve a microservice call from my Kafka Consumer. Let's say if the Microservice is down then I need to notify my Kafka consumer using a circuit breaker pattern to stop fetching the messages/events until the Microservice is up and running.
With Spring Kafka, you could use the pause and resume methods depending on the CircuitBreaker state transitions. The best way I found for this is to define it as "supervisor" with an #Configuration Annotation. Resilience4j is also used.
#Configuration
public class CircuitBreakerConsumerConfiguration {
public CircuitBreakerConsumerConfiguration(CircuitBreakerRegistry circuitBreakerRegistry, KafkaManager kafkaManager) {
circuitBreakerRegistry.circuitBreaker("yourCBName").getEventPublisher().onStateTransition(event -> {
switch (event.getStateTransition()) {
case CLOSED_TO_OPEN:
case CLOSED_TO_FORCED_OPEN:
case HALF_OPEN_TO_OPEN:
kafkaManager.pause();
break;
case OPEN_TO_HALF_OPEN:
case HALF_OPEN_TO_CLOSED:
case FORCED_OPEN_TO_CLOSED:
case FORCED_OPEN_TO_HALF_OPEN:
kafkaManager.resume();
break;
default:
throw new IllegalStateException("Unknown transition state: " + event.getStateTransition());
}
});
}
}
This is what I used in combination with a KafkaManager annotated with #Component.
#Component
public class KafkaManager {
private final KafkaListenerEndpointRegistry registry;
public KafkaManager(KafkaListenerEndpointRegistry registry) {
this.registry = registry;
}
public void pause() {
registry.getListenerContainers().forEach(MessageListenerContainer::pause);
}
public void resume() {
registry.getListenerContainers().forEach(MessageListenerContainer::resume);
}
}
In addition my consumer service looks like this:
#KafkaListener(topics = "#{'${topic.name}'}", concurrency = "1", id = "CBListener")
public void receive(final ConsumerRecord<String, ReplayData> replayData, Acknowledgment acknowledgment) throws
Exception {
try {
httpClientServiceCB.receiveHandleCircuitBreaker(replayData);
acknowledgement.acknowledge();
} catch (Exception e) {
acknowledgment.nack(1000);
}
}
And the #CircuitBreaker Annotation:
#CircuitBreaker(name = "yourCBName")
public void receiveHandleCircuitBreaker(ConsumerRecord<String, ReplayData> replayData) throws
Exception {
try {
String response = restTemplate.getForObject("http://localhost:8081/item", String.class);
} catch (Exception e ) {
// throwing the exception is needed to trigger the Circuit Breaker state change
throw new Exception();
}
}
And this is additionally supplemented by the following application.properties
resilience4j.circuitbreaker.instances.yourCBName.failure-rate-threshold=80
resilience4j.circuitbreaker.instances.yourCBName.sliding-window-type=COUNT_BASED
resilience4j.circuitbreaker.instances.yourCBName.sliding-window-size=5
resilience4j.circuitbreaker.instances.yourCBName.wait-duration-in-open-state=10000
resilience4j.circuitbreaker.instances.yourCBName.automatic-transition-from-open-to-half-open-enabled=true
spring.kafka.consumer.enable.auto.commit = false
spring.kafka.listener.ack-mode = MANUAL_IMMEDIATE
Also have a look at https://resilience4j.readme.io/docs/circuitbreaker
If you are using Spring Kafka, you could maybe use the pause and resume methods of the ConcurrentMessageListenerContainer class.
You can attach an EventListener to the CircuitBreaker which listens on state transitions and pauses or resumes processing of events. Inject the CircuitBreakerRegistry into you bean:
circuitBreakerRegistry.circuitBreaker("yourCBName").getEventPublisher().onStateTransition(
event -> {
switch (event.getStateTransition()) {
case CLOSED_TO_OPEN:
container.pause();
case OPEN_TO_HALF_OPEN:
container.resume();
case HALF_OPEN_TO_CLOSED:
container.resume();
case HALF_OPEN_TO_OPEN:
container.pause();
case CLOSED_TO_FORCED_OPEN:
container.pause();
case FORCED_OPEN_TO_CLOSED:
container.resume();
case FORCED_OPEN_TO_HALF_OPEN:
container.resume();
default:
}
}
);

Spring Integration: start new transaction in my Message flow IntegrationFlowBuilder to commit change and resume the outer transaction

My jdbcSourceMessage execute a select for update with batch of 100 rows at a time.
While the integrationFlow is been executed in a Transaction to hold a lock to the database for the fetched batch.
I would like to start new Transaction for my JdbcSourceUpdate (within the message flow) to excute an update and commit my change for each
row sent throught the channel.
#Bean
public IntegrationFlow integrationFlow() {
IntegrationFlowBuilder flowBuilder = IntegrationFlows.from(jdbcSourceMessage());
flowBuilder
.split()
.log(LoggingHandler.Level.INFO, message ->
message.getHeaders().get("sequenceNumber")
+ " événements publiés sur le bus de message sur "
+ message.getHeaders().get("sequenceSize")
+ " événements lus (lot)")
.transform(Transformers.toJson())
.log()
.enrichHeaders(h -> h.headerExpression("type", "payload.typ_evenement"))
.publishSubscribeChannel(publishSubscribeSpec -> publishSubscribeSpec
.subscribe(flow -> flow
.bridge()
.transform(Transformers.toJson())
.transform(kafkaGuyTransformer())
.channel(this.rabbitMQchannel.demandeInscriptionOutput()))
.subscribe(flow -> flow
.handle(jdbcMessageHandler()))
);
return flowBuilder.get();
}
#Bean(name = PollerMetadata.DEFAULT_POLLER)
public PollerMetadata defaultPoller() {
PeriodicTrigger trigger = new PeriodicTrigger(this.proprietesSourceJdbc.getTriggerDelay(), TimeUnit.SECONDS);
PollerMetadata pollerMetadata = Pollers.trigger(trigger)
.advice(transactionInterceptor())
.get();
pollerMetadata.setMaxMessagesPerPoll(proprietesSourceJdbc.getMaxRowsPerPoll());
return pollerMetadata;
}
#Bean
public JdbcSourceUpdate jdbcSourceUpdate() {
return new JdbcSourceUpdate();
}
public TransactionInterceptor transactionInterceptor() {
return new TransactionInterceptorBuilder()
.transactionManager(transactionManager())
.build();
}
public PlatformTransactionManager transactionManager(){
DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(sourceDeDonnees);
transactionManager.setRollbackOnCommitFailure(false);
return transactionManager;
}
public class KafkaGuyTransformer implements GenericTransformer<Message, Message> {
#Override
public Message transform(Message message) {
Message<String> msg = null;
try {
DemandeRecueDTO dto = objectMapper.readValue(message.getPayload().toString(), DemandeRecueDTO.class);
msg = MessageBuilder.withPayload(dto.getTxtDonnee())
.copyHeaders(message.getHeaders())
.build();
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
return msg;
}
}
public class JdbcSourceUpdate implements MessageHandler {
#Override
public void handleMessage(Message<?> message) throws MessagingException {
try {
Thread.sleep(100);
DemandeRecueDTO dto = objectMapper.readValue(message.getPayload().toString(), DemandeRecueDTO.class);
jdbcTemplate.update(proprietesSourceJdbc.getUpdate(), dto.getIdEvenementDemandeCrcd());
} catch (Exception e) {
LOG.error(e.getMessage(), e);
}
}
}
Since you have that JdbcSourceUpdate implementation, there is just enough to do like this:
#Transactional(propagation = Propagation.REQUIRES_NEW)
#Override
public void handleMessage(Message<?> message) throws MessagingException {
See its JavaDocs for more info:
/**
* Create a new transaction, and suspend the current transaction if one exists.
* Analogous to the EJB transaction attribute of the same name.
* <p><b>NOTE:</b> Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {#link org.springframework.transaction.jta.JtaTransactionManager},
* which requires the {#code javax.transaction.TransactionManager} to be
* made available to it (which is server-specific in standard Java EE).
* #see org.springframework.transaction.jta.JtaTransactionManager#setTransactionManager
*/
REQUIRES_NEW(TransactionDefinition.PROPAGATION_REQUIRES_NEW),
UPDATE
Pay attention to the NOTE though:
* Actual transaction suspension will not work out-of-the-box
* on all transaction managers. This in particular applies to
* {#link org.springframework.transaction.jta.JtaTransactionManager}`.
So, sounds like DataSourceTransactionManager doesn't work with suspension. I can suggest you to consider to use a .gateway() for that JdbcSourceUpdate, but using an ExecutorChannel. This way your handle(jdbcSourceUpdate() will be performed on a new thread and, therefore, with a new transaction. The main flow will wait for the reply from that gateway holding its transaction opened.
Something like this:
.subscribe(f -> f
.gateway(subFlow ->
subFlow.channel(c -> c.executor())
.handle(jdbcMessageHandler()))
.channel("nullChannel")
));
Buy your JdbcSourceUpdate must return something for the gateway reply. Consider do not implement MessageHandler there, but make just as a plain POJO with a single non-void method.

Spring rabbit retries to deliver rejected message..is it OK?

I have the following configuration
spring.rabbitmq.listener.prefetch=1
spring.rabbitmq.listener.concurrency=1
spring.rabbitmq.listener.retry.enabled=true
spring.rabbitmq.listener.retry.max-attempts=3
spring.rabbitmq.listener.retry.max-interval=1000
spring.rabbitmq.listener.default-requeue-rejected=false //I have also changed it to true but the same behavior still happens
and in my listener I throw the exception AmqpRejectAndDontRequeueException to reject the message and enforce rabbit not to try to redeliver it...But rabbit redilvers it for 3 times then finally route it to dead letter queue.
Is that the standard behavior according to my provided configuration or do I miss something?
You have to configure the retry policy to not retry for that exception.
You can't do that with properties, you have to configure the retry advice yourself.
I'll post an example later if you need help with that.
requeue-rejected is at the container level (below retry on the stack).
EDIT
#SpringBootApplication
public class So39853762Application {
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext context = SpringApplication.run(So39853762Application.class, args);
Thread.sleep(60000);
context.close();
}
#RabbitListener(queues = "foo")
public void foo(String foo) {
System.out.println(foo);
if ("foo".equals(foo)) {
throw new AmqpRejectAndDontRequeueException("foo"); // won't be retried.
}
else {
throw new IllegalStateException("bar"); // will be retried
}
}
#Bean
public ListenerRetryAdviceCustomizer retryCustomizer(SimpleRabbitListenerContainerFactory containerFactory,
RabbitProperties rabbitPropeties) {
return new ListenerRetryAdviceCustomizer(containerFactory, rabbitPropeties);
}
public static class ListenerRetryAdviceCustomizer implements InitializingBean {
private final SimpleRabbitListenerContainerFactory containerFactory;
private final RabbitProperties rabbitPropeties;
public ListenerRetryAdviceCustomizer(SimpleRabbitListenerContainerFactory containerFactory,
RabbitProperties rabbitPropeties) {
this.containerFactory = containerFactory;
this.rabbitPropeties = rabbitPropeties;
}
#Override
public void afterPropertiesSet() throws Exception {
ListenerRetry retryConfig = this.rabbitPropeties.getListener().getRetry();
if (retryConfig.isEnabled()) {
RetryInterceptorBuilder<?> builder = (retryConfig.isStateless()
? RetryInterceptorBuilder.stateless()
: RetryInterceptorBuilder.stateful());
Map<Class<? extends Throwable>, Boolean> retryableExceptions = new HashMap<>();
retryableExceptions.put(AmqpRejectAndDontRequeueException.class, false);
retryableExceptions.put(IllegalStateException.class, true);
SimpleRetryPolicy policy =
new SimpleRetryPolicy(retryConfig.getMaxAttempts(), retryableExceptions, true);
ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
backOff.setInitialInterval(retryConfig.getInitialInterval());
backOff.setMultiplier(retryConfig.getMultiplier());
backOff.setMaxInterval(retryConfig.getMaxInterval());
builder.retryPolicy(policy)
.backOffPolicy(backOff)
.recoverer(new RejectAndDontRequeueRecoverer());
this.containerFactory.setAdviceChain(builder.build());
}
}
}
}
NOTE: You cannot currently configure the policy to retry all exceptions, "except" this one - you have to classify all exceptions you want retried (and they can't be a superclass of AmqpRejectAndDontRequeueException). I have opened an issue to support this.
The other answers posted here didn't work me when using Spring Boot 2.3.5 and Spring AMQP Starter 2.2.12, but for these versions I was able to customize the retry policy to not retry AmqpRejectAndDontRequeueException exceptions:
#Configuration
public class RabbitConfiguration {
#Bean
public RabbitRetryTemplateCustomizer customizeRetryPolicy(
#Value("${spring.rabbitmq.listener.simple.retry.max-attempts}") int maxAttempts) {
SimpleRetryPolicy policy = new SimpleRetryPolicy(maxAttempts, Map.of(AmqpRejectAndDontRequeueException.class, false), true, true);
return (target, retryTemplate) -> retryTemplate.setRetryPolicy(policy);
}
}
This lets the retry policy skip retries for AmqpRejectAndDontRequeueExceptions but retries all other exceptions as usual.
Configured this way, it traverses the causes of an exception, and skips retries if it finds an AmqpRejectAndDontRequeueException.
Traversing the causes is needed as org.springframework.amqp.rabbit.listener.adapter.MessagingMessageListenerAdapter#invokeHandler wraps all exceptions as a ListenerExecutionFailedException

Testing that delete is correctly rolled back with DataIntegrityViolationException junit, spring, #Transactional

I have a category -> subCategory -> products hierarchy in my application. If a subcategory has no products, you are allowed to delete it. If a subCategory has products, the DAO throws a DataIntegrityViolationException and the transaction should be rolled back.
In my tests, I have:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = {TestTransactionManagement.class})
public class BusinessSubCategoryCRUDTest {
#Autowired
public void setCRUD(BusinessSubCategoryCRUD crud) {
this.crud = crud;
}
// #Transactional
#Test
public void testDeleteBusinessSubCategoryInUseCanNotBeDeleted() {
final long id = 1;
BusinessSubCategory subCategoryBeforeDelete =
crud.readBusinessSubCategory(id);
final int numCategoriesBeforeDelete =
subCategoryBeforeDelete.getBusinessCategories().size();
try {
crud.deleteBusinessSubCategory(
new BusinessSubCategory(id, ""));
} catch (DataIntegrityViolationException e) {
System.err.println(e);
}
BusinessSubCategory subCategoryAfterDeleteFails =
crud.readBusinessSubCategory(id);
// THIS next assertion is the source of my angst.
// At this point the the links to the categories will have been
// been deleted, an exception will have been thrown but the
// Transaction is not yet rolled back if the test case (or test
// class) is marked with #Transactional
assertEquals(
numCategoriesBeforeDelete,
subCategoryAfterDeleteFails.getBusinessCategories().size());
}
}
However, if I uncomment the #Transactional above #Test, it fails. I think the DAO is using the transaction from the #Test and so the transaction doesn't roll back until AFTER I check to be sure the transaction has been rolled back.
#Transactional(readOnly = false, propagation =
Propagation.REQUIRED)
public boolean deleteBusinessSubCategory(
BusinessSubCategory businessSubCategory) {
BeanPropertySqlParameterSource paramMap = new
BeanPropertySqlParameterSource(businessSubCategory);
namedJdbcTemplate.update(
DELETE_CATEGORY_SUB_CATEGORY_BY_ID_SQL,
paramMap);
return 0 != namedJdbcTemplate.update(
DELETE_SUB_CATEGORY_BY_ID_SQL,
paramMap);
}
So, how do I have the DAO code still inherit the transaction from the context it is running in (in production it inherits the transaction from the service it is running in) but still be able to test it. I want to put #Transactional on the entire test class, but that then leaves my test either failing or incomplete.
For completeness, here is my configuration class for the test.
#Configuration
#EnableTransactionManagement
public class TestTransactionManagement {
#Bean
public EmbeddedDatabase getDataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder
.setType(EmbeddedDatabaseType.HSQL) //.H2 or .DERBY
.addScript("sql/create-db.sql")
.addScript("sql/create-test-data.sql")
.build();
return db;
}
#Bean
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
#Bean
public BusinessSubCategoryCRUD getCRUD() {
return new BusinessSubCategoryCRUD(getDataSource());
}
}
The "solution" or workaround was to reset the database before each test. Then there was no need for an #Transactional on the test, the rollback could be tested, and the test suite ran slighly slower due to the additional database setup.
#Before
public void setUp() {
Connection conn = DataSourceUtils.getConnection(dataSource);
ScriptUtils.executeSqlScript(
conn, new ClassPathResource("sql/create-test-data.sql"));
DataSourceUtils.releaseConnection(conn, dataSource);
}

Resources