#Override
#Transactional(transactionManager="db1PlatformTransactionManager", propagation= Propagation.REQUIRED_NEW, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED)
public Account creditAmount(AccountDto accountDto, BigDecimal creditAmt) throws EntityNotFoundException{
throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString());
}
#Override
#Transactional(transactionManager="db1PlatformTransactionManager", propagation= Propagation.REQUIRED_NEW, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED)
public Account debitAmount(AccountDto accountDto, BigDecimal debitAmt) throws EntityNotFoundException, InsufficientFundsException {
assert(debitAmt.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0
Optional<Account> accountInDb = accountRepository.findById(accountDto.getAccountNumber());
if (accountInDb.isPresent()) {
Account account = accountInDb.get();
account.debit(debitAmt);
return accountRepository.save(account);
}
throw new EntityNotFoundException(Account.class, "accountNumber", accountDto.getAccountNumber().toString());
}
#Override
#Transactional(transactionManager="db1PlatformTransactionManager", propagation=Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED)
public List<Account> transferFunds(AccountDto debitAccountDto, AccountDto creditAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException {
assert(amount.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0
Account debitAccount = debitAmount(debitAccountDto, amount);
Account creditAccount = creditAmount(creditAccountDto, amount); // throws exception
return Stream
.of(debitAccount, creditAccount)
.collect(Collectors.toList());
}
as you see creditAmount method throws exception and debitAmount method have PRAPOGATION as REQUIRED_NEW. so excepted as debitAmount should commit data but it get rollback as well
As mentioned here :
Spring proxify your method with transaction inside the same class, so calling a nested method of the same class will not go through the interceptor, but directly through your own instance, that lead you to lose all the transaction support.
TO avoid this you can go with a self proxy with two steps:
STEP 1: create a proxy of your current service and inject it as any other beans
STEP 2: For internal method, use the proxyBean created in step one. it should look like:
//#Autowired
//or instancied by your constructror
private YourCurrentService selfProxified;
#Override
#Transactional(transactionManager="db1PlatformTransactionManager", propagation=Propagation.REQUIRED, rollbackFor = {InsufficientFundsException.class, EntityNotFoundException.class}, isolation=Isolation.READ_COMMITTED)
public List<Account> transferFunds(AccountDto debitAccountDto, AccountDto creditAccountDto, BigDecimal amount) throws EntityNotFoundException, InsufficientFundsException {
assert(amount.compareTo(BigDecimal.ZERO) == 1); //assert greater than 0
Account debitAccount = selfProxified.debitAmount(debitAccountDto, amount);
Account creditAccount = selfProxified.creditAmount(creditAccountDto, amount); // throws exception
return Stream
.of(debitAccount, creditAccount)
.collect(Collectors.toList());
}
Related
I have two transaction manager for two database. I need to persist same data into both databases. If one transaction failed, other one need rollback. I have done like below
public interface DataService {
void saveData();
}
#Service
public class DataServiceImpl implements DataService {
#Autowired
private DataRepository dataRepository;
#Autowired
private OrDataRepository orDataRepository;
#Autowired
#Qualifier("orService")
private OrService orDataServiceImpl;
#Override
#Transactional(transactionManager = "transactionManager", rollbackFor = {RuntimeException.class})
public void saveData() {
Data data = new Data();
data.setCompKey(UUID.randomUUID().toString().substring(1,5));
data.setName("data");
dataRepository.save(data);
orDataServiceImpl.save();
//throw new RuntimeException("");
}
}
public interface OrService {
void save();
}
#Service("orService")
public class OrDataServiceImpl implements OrService {
#Autowired
private OrDataRepository orDataRepository;
#Override
#Transactional(rollbackFor = {RuntimeException.class})
public void save() {
OrData data = new OrData();
data.setCompKey(UUID.randomUUID().toString().substring(1,5));
data.setName("ordata");
orDataRepository.save(data);
}
}
I have two transaction manager (entityManager & orEntityManager) for two different DB.
If any exception in OrDataServiceImpl save method, data is not getting persisted in both DB. But if any exception in DataServiceImpl saveData method, data is getting persisted into OrData table.
I want to rollback the data from both DB if any exception.
chainedTransactionManager is deprecated. So can't use. atomikos and bitronix also can't use due to some restrictions. Kindly suggest better way to achieve distributed transation
The code need to be refactored, edit the DataServiceImpl.save() method.
Comment the orDataServiceImpl.save() line
public void saveData() {
Data data = new Data();
data.setCompKey(UUID.randomUUID().toString().substring(1,5));
data.setName("data");
dataRepository.save(data);
//orDataServiceImpl.save();
//throw new RuntimeException("");
}
Refactor/Edit the OrDataService Interface
public interface OrDataService {
void save(String uuid);
void delete(String uuid);
//will be use for compensating transaction
}
Update the OrDataServiceImpl class to implement above interface
Write new orchestration Method and use compensating transaction to rollback
pseudo code
call OrDataServiceImpl.save()
if step#1 was success
-> DataServiceImpl.saveData()
if Exception at step#3,
->OrDataServiceImpl.delete() [//to rollback]
else if, Exception at step#1
//do nothing
I implemented feign client and hystrix to my spring boot microservice application.
I first tried to test to communicate users service to albums service with feign client,
so I threw an exception at albums service to check if users service Error Decoder can catch the exception and then make the fallback method triggered.
It worked, but the cause is always null only at the first time, and after that I can see the error message that I wanted to see.
Can anyone tell me if something is wrong or not.
This is my code.
Users Service Feign Client
#FeignClient(name = "albums-ws", fallbackFactory = AlbumsFallbackFactory.class)
public interface AlbumServiceClient {
#GetMapping(path = "users/{userId}/albums")
List<AlbumDetailResponse> getAlbums(#PathVariable("userId") String userId);
}
Fallback Factory
#Component
public class AlbumsFallbackFactory implements FallbackFactory<AlbumServiceClient> {
#Override
public AlbumServiceClient create(Throwable cause) {
return new AlbumServiceClientFallback(cause);
}
}
public class AlbumServiceClientFallback implements AlbumServiceClient {
private final Throwable cause;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
public AlbumServiceClientFallback(Throwable cause) {
this.cause = cause;
}
#Override
public List<AlbumDetailResponse> getAlbums(String userId) {
logger.error("An exception took place: " + cause.getMessage());
return new ArrayList<>();
}
}
Feign Error Decoder
#Component
public class FeignErrorDecoder implements ErrorDecoder {
#Override
public Exception decode(String methodKey, Response response) {
switch(response.status()) {
case 400:
break;
case 404:
if(methodKey.contains("getAlbums")) {
return new ResponseStatusException(HttpStatus.valueOf(response.status()), response.reason());
}
break;
default:
return new Exception(response.reason());
}
return null;
}
}
First fallback triggered
2020-08-02 12:42:27.836 ERROR 24772 --- [ HystrixTimer-1] c.a.p.a.u.P.f.AlbumServiceClientFallback : An exception took place: null
After
2020-08-02 12:43:07.672 DEBUG 24772 --- [rix-albums-ws-2] c.a.p.a.u.P.feign.AlbumServiceClient : [AlbumServiceClient#getAlbums] User not found with id: f5b313e2-411f-4fc3-95e7-9aa5c43c286c
Hystrix has class org.springframework.cloud.netflix.feign.HystrixTargeter. There is a comment in targetWithFallbackFactory method:
We take a sample fallback from the fallback factory to check if it
returns a fallback that is compatible with the annotated feign
interface.
and code after:
Object exampleFallback = fallbackFactory.create(new RuntimeException());
It is why you don't have cause in exception.
#Component
public class CustomHealthIndicator extends AbstractHealthIndicator {
#Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// Use the builder to build the health status details that should be reported.
// If you throw an exception, the status will be DOWN with the exception message.
builder.up()
.withDetail("app", "Alive and Kicking")
.withDetail("error", "Nothing! I'm good.");
}
}
Here I noticed default health check happen through /health, I want to override something like above when dataConnection pool is short of available connection I need to return pod not ready. also i need to check health of the database. how to implement that?
I did something similar for ages.
Based in my experience with HikariDataSource that was my result:
#Autowired
private DataSource dataSource;
#Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
HikariDataSource ds = (HikariDataSource) dataSource;
Integer maxSize = ds.getMaximumPoolSize();
Integer active = new HikariDataSourcePoolMetadata(ds).getActive();
Double usage = new Double((active / maxSize) * 100);
Health.Builder workingBuilder;
if(usage > 90) {
workingBuilder = builder.down();
}else {
workingBuilder = builder.up();
}
workingBuilder.withDetail("max", maxSize) //
.withDetail("active", active)//
.withDetail("usage", usage);
}
Maybe there is a better approach by Actuator Metrics or with
ds.getHealthCheckProperties()
ds.getHealthCheckRegistry()
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);
}
I have spring junit test consisting of two sequential transactions which are propagated as REQUIRES_NEW:
public class ContractServiceTest extends AbstractIntegrationTest {
#Autowired
private PersistenceManagerHibernate persistenceManagerHibernate;
#Autowired
private ContractService contractService;
#Autowired
private EntityChangeService entityChangeService;
#Resource
private AddServiceService addService;
#Autowired
private ReferenceBookService refService;
#Autowired
private PropertyService propertyService;
#Autowired
private HibernateTransactionManager transactionManager;
#Test
public void testContractDeletes() {
Long contractId = 1L;
final Contract contract = createTestDetachedContract(contractId, PropertyServiceTest.createManaged(propertyService, refService), refService);
ensureContractCreated(contract);
deleteTransactional(contract);
Assert.assertEquals(1, entityChangeService.findByPaginationOrderByUpdateDate(Contract.class.getName(), contract.getId().toString(), null, 0, 30).size());
}
#Test
#Ignore
public void testContractCreates() {
Long contractId = 1L;
final Contract contract = createTestDetachedContract(contractId, PropertyServiceTest.createManaged(propertyService, refService), refService);
ensureContractDeleted(contract);
createContractTransactional(contract);
Assert.assertEquals(1, entityChangeService.findByPaginationOrderByUpdateDate(Contract.class.getName(), contract.getId().toString(), null, 0, 30).size());
}
private void ensureContractCreated(Contract contract) {
if (persistenceManagerHibernate.isCreated(Contract.class, contract.getId())) {
return;
}
createContractTransactional(contract);
}
private void deleteTransactional(final Contract contract) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
transactionTemplate.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
try {
contractService.delete(contract);
} catch (Exception e) {
toString();
}
return null;
}
});
}
private void createContractTransactional(final Contract contract) {
TransactionTemplate transactionTemplate2 = new TransactionTemplate(transactionManager);
transactionTemplate2.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
transactionTemplate2.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
contractService.create(contract);
return null;
}
});
}
private void ensureContractDeleted(final Contract contract) {
if (!persistenceManagerHibernate.isCreated(Contract.class, contract.getId())) {
return;
}
deleteTransactional(contract);
}
public static Contract createTestDetachedContract(Long contractId, Property property, ReferenceBookService refService) {
Contract contract1 = new Contract();
contract1.setId(contractId);
contract1.setName("test name");
contract1.setProperty(property);
contract1.setNumber("10");
contract1.setType(refService.get(ContractType.class, 1L));
contract1.setStatus(refService.get(ContractStatus.class, 1L));
contract1.setCreated(new Date());
contract1.setCurrencyRate(new BigDecimal(10));
contract1.setInitialSum(new BigDecimal(10));
contract1.setSum(new BigDecimal(10));
return contract1;
}
}
Test freezes at commiting of transaction with insert sql statement, which is:
private void createContractTransactional(final Contract contract) {
TransactionTemplate transactionTemplate2 = new TransactionTemplate(transactionManager);
transactionTemplate2.setPropagationBehavior(Propagation.REQUIRES_NEW.value());
transactionTemplate2.execute(new TransactionCallback() {
public Object doInTransaction(TransactionStatus status) {
contractService.create(contract);
return null;
}
});
}
Why does that happening(debugger stops at some oracle code without source code provided) and how to write spring junit test with two sequential transactions correctly?
It sounds like the test is creating a deadlock on the Contract table in your database. The root cause of this is most likely the use of the REQUIRES_NEW propagation level, as detailed in this question. The important part is this:
PROPAGATION_REQUIRES_NEW starts a new, independent "inner" transaction for the given scope. This transaction will be committed or rolled back completely independent from the outer transaction, having its own isolation scope, its own set of locks, etc. The outer transaction will get suspended at the beginning of the inner one, and resumed once the inner one has completed
The createContractTransactional method is trying to insert into the Contract table but something earlier in the test must be holding a lock on it, I'm guessing it's the call to persistenceManagerHibernate.isCreated(Contract.class, contract.getId()). Whatever the cause, you have two independent transactions that are locking on the same table, i.e. a deadlock.
Try setting the propagation level the transactions in your test to REQUIRED, which is the default setting. This creates a new transaction if there isn't one already, otherwise the current transaction is used. That should make your test execute in a single transaction and so you shouldn't get a deadlock. Once that is working then you may want to read the spring documentation on its propagation levels to make sure that REQUIRED is the right level for your needs.