Spring Boot Service Testing - spring

I have a problem with testing my application.
I want to test this method:
public Optional<UserDto> getUser(String username) {
Optional<User> userOptional = this.userRepository.findById(username);
if (userOptional.isPresent()) {
UserDto userDto = this.convert(userOptional.get());
return Optional.of(userDto);
}
return Optional.empty();
}
I tried to test it, but the line coverage shows that inside the if statement is not checked.
The tests I wrote:
#Test
void getUserShouldReturnAnOptionalOfUserDtoIfItIsFound() {
Optional<UserDto> userDtoOptional = this.underTest.getUser(user.getUsername());
Mockito.verify(userRepository).findById(user.getUsername());
userDtoOptional.ifPresent(userDto ->
Assertions.assertThat(userDto).isInstanceOf(UserDto.class));
}
#Test
void getUserTestShouldReturnOptionalEmptyWhenUserIsNotFound() {
String username = "johndoe";
Optional<UserDto> userDtoOptional = this.underTest.getUser(username);
Mockito.verify(userRepository).findById(username);
userDtoOptional.ifPresent(userDto ->
Assertions.assertThat(userDto).isInstanceOf(Optional.empty().getClass()));
}
Thank you if you have an advice!

Related

LoadingCache mocking for JUnit testing

I need to test this method.
public String getTenantName(String tenantId) {
var tenant = getTenant(tenantId);
if (tenant == null) {
throw new TenantNotFoundException(tenantId);
}
return tenant.getTenantname();
}
but I am having problems with mocking the below loading cache
LoadingCache<String, Tenant> tenantCache = CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterAccess(24, TimeUnit.HOURS).build(new CacheLoader<String, Tenant>() {
#Override
public Tenant load(String tenantId) {
return tenantClient.getTenant(tenantId);
}
});
as this is being called by another private method
private Tenant getTenant(String tenantId) {
try {
if (StringUtils.isBlank(tenantId)) {
return null;
}
return tenantCache.get(tenantId);
} catch (TenantNotFoundException | ExecutionException e) {
logger.error(tenantId, e);
throw new TenantNotFoundException(tenantId);
}
}
I would really appreciate some help here.
I mocked loading cache
#mock
LoadingCache<String, Tenant> tenantCache;
and then in my test function I create a tenant object and return that on tenantCache.get() call.
tenantCache = CacheBuilder.newBuilder().maximumSize(1000)
.expireAfterAccess(24, TimeUnit.HOURS).build(new CacheLoader<String, Tenant>() {
#Override
public Tenant load(String tenantId) {
return tenantClient.getTenant(tenantId);
}
});
Map<String, Tenant> map = new HashMap<String, Tenant>();
map.put("test", tenant);
tenantCache.putAll(map);
also for tenantClient I changed that to return tenant.
return tenantClient.getTenant(id) =>> return tenant;
as tenantClient is calling another API.
So, LoadingCache appears as a variable inside the service but it is implemented as an anonymous class. Therefore we need to mock LoadingCache and use
when(tenantCache.get(anyString())).thenReturn(new Tenant());

Mockito tests pass except one verify

I have all my tests pass except this line in the first test
verify(reimbursementDAO).getById(REIMBURSEMENT_TO_PROCESS.getId());
see code below.
package com.revature.services;
public class ReimbursementServiceTest {
private static ReimbursementService reimbursementService;
private static ReimbursementDAO reimbursementDAO;
private Reimbursement REIMBURSEMENT_TO_PROCESS;
private Reimbursement GENERIC_REIMBURSEMENT_1;
private Optional<Reimbursement>
GENERIC_REIMBURSEMENT_2;
private List<Reimbursement> GENERIC_ALL_PENDING_REIMBURSEMENTS;
private User GENERIC_EMPLOYEE_1;
private User GENERIC_FINANCE_MANAGER_1;
#BeforeClass
public static void setUpBeforeClass() throws Exception {
reimbursementDAO=mock(ReimbursementDAO.class);//(IReimbursementDAO.class);
reimbursementService = new ReimbursementService(reimbursementDAO);
//reimbursementDAO=new ReimbursementDAO();
}
#Before
public void setUp() throws Exception {
GENERIC_EMPLOYEE_1 = new User(1, "genericEmployee1", "genericPassword", Role.EMPLOYEE);
GENERIC_FINANCE_MANAGER_1 = new User(1, "genericManager1", "genericPassword", Role.FINANCE_MANAGER);
REIMBURSEMENT_TO_PROCESS = new Reimbursement(2, Status.PENDING, GENERIC_EMPLOYEE_1, null, 150.00);
GENERIC_REIMBURSEMENT_1 = new Reimbursement(1, Status.PENDING, GENERIC_EMPLOYEE_1, null, 100.00);
GENERIC_REIMBURSEMENT_2 = Optional.ofNullable(new Reimbursement(2, Status.APPROVED, GENERIC_EMPLOYEE_1,
GENERIC_FINANCE_MANAGER_1, 150.00));
GENERIC_ALL_PENDING_REIMBURSEMENTS = new ArrayList<Reimbursement>();
GENERIC_ALL_PENDING_REIMBURSEMENTS.add(GENERIC_REIMBURSEMENT_1);
}
#Test
public void testProcessPassesWhenUserIsFinanceManagerAndReimbursementExistsAndUpdateSuccessful()
throws Exception{
when(reimbursementDAO.getById(anyInt())).thenReturn(Optional.of(GENERIC_REIMBURSEMENT_1));
when(reimbursementDAO.update(any())).thenReturn(GENERIC_REIMBURSEMENT_2);
assertEquals(GENERIC_REIMBURSEMENT_2,
reimbursementService.process(REIMBURSEMENT_TO_PROCESS, Status.APPROVED,
GENERIC_FINANCE_MANAGER_1));
//verify(reimbursementDAO).getById(REIMBURSEMENT_TO_PROCESS.getId());
verify(reimbursementDAO).update(REIMBURSEMENT_TO_PROCESS);
}
#Test
public void testGetReimbursementByStatusPassesWhenReimbursementsAreSuccessfullyReturned() {
when(reimbursementDAO.getBystatus(any())).thenReturn(GENERIC_ALL_PENDING_REIMBURSEMENTS);
assertEquals(GENERIC_ALL_PENDING_REIMBURSEMENTS,
reimbursementService.getReimbursementsByStatus(Status.PENDING));
verify(reimbursementDAO).getBystatus(Status.PENDING);
}
}
public class ReimbursementDAO extends AbstractReimbursement
{
public Optional< Reimbursement> getById(int id) {
try(Connection conn = ConnectionFactory.getConnection())
{
String sql="select * from ers_reimbursements where reimb_id=?;";
PreparedStatement ps=conn.prepareStatement(sql);
ps.setInt(1,id);
ResultSet rs= ps.executeQuery();
Reimbursement reimb=null;
UserService usrv=new UserService();
//reimb_id ,amount, submitted,resolved,description,author,receipt ,resolver,status,reimb_type
while(rs.next())
{
int reid=rs.getInt("reimb_id");
double ramount=rs.getInt("reimb_amount");
int res=rs.getInt( "resolver");
User resolver=null;
String description=rs.getString("description");
User rauthor= usrv.getUserById( rs.getInt("author")).get();
if(res>0)
{ resolver= usrv.getUserById(res).get(); }
int rstatus= rs.getInt("reimb_status");
Status r_status=Status.values()[--rstatus];
int reimb_type= rs.getInt("reimb_type");
ReimbType retype=ReimbType.values()[--reimb_type];
User oth=rauthor;
User re=resolver;
reimb=new Reimbursement(reid, r_status,oth,re,ramount);
return Optional.ofNullable(reimb);
}
}catch(SQLException e) { e.printStackTrace();};
return Optional.empty();
}
public List<Reimbursement> getBystatus(Status status) {
try(Connection conn = ConnectionFactory.getConnection())
{
String sql="select * from ers_reimbursements where reimb_status=?;";
PreparedStatement ps=conn.prepareStatement(sql);//,Statement.RETURN_GENERATED_KEYS);
int sta_id= status.ordinal()+1;
ps.setInt(1,sta_id);
ResultSet rs= ps.executeQuery();
Reimbursement reimb=null;
List<Reimbursement> reimbList=new ArrayList<Reimbursement>();
IUserService usrv=new UserService();
//reimb_id ,amount, submitted,resolved,description,author,receipt ,resolver,status,reimb_type
while(rs.next())
{
//int id, Status status, User author, User resolver, double amount
int reid=rs.getInt("reimb_id");
double ramount=rs.getInt("reimb_amount");
Optional<User> rauthor= usrv.getUserById( rs.getInt("author"));
User oth=null;
if(rauthor.isPresent())
{ oth=rauthor.get(); }
int resol=rs.getInt( "resolver");
Optional<User> resolver= usrv.getUserById(resol);
User re=null;
if(resolver.isPresent())
{ re=resolver.get(); }
else {re=null;}
int rstatus= rs.getInt("reimb_status");
Status r_status=Status.values()[--rstatus];//.PENDING;
int reimb_type= rs.getInt("reimb_type");
ReimbType retype=ReimbType.values()[--reimb_type];//.TRAVEL;
reimb=new Reimbursement(reid, r_status,oth,re,ramount);
reimbList.add(reimb);
}
return reimbList;
}catch(SQLException e) { e.printStackTrace();};
return null;
}
public Optional<Reimbursement> update(Reimbursement unprocessedReimbursement) {
try(Connection conn=ConnectionFactory.getConnection()) {
String sql="update ers_reimbursements set reimb_status=?,"
+ " resolver=?, resolved=? where reimb_id=?;";
PreparedStatement ps=conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
int id=unprocessedReimbursement.getId();
Status st=unprocessedReimbursement.getStatus();
ps.setObject(1,st);
ps.setInt(2,unprocessedReimbursement.getResolver().getId());
ps.setObject(3, LocalDateTime.now());
ps.setInt(4,id);
ps.executeUpdate();
try (ResultSet generatedKeys = ps.getGeneratedKeys()) {
if (generatedKeys.next()) {
int reimid=generatedKeys.getInt(1);
Optional<Reimbursement> reim=getById(reimid);
System.out.println("Reimb " + reim.get()+ " upLocalTimed!");
return reim;
}
}catch(SQLException e) {};
}catch(SQLException e) { e.printStackTrace();}
return Optional.empty();
}
}
public class ReimbursementService{
{
private final ReimbursementDAO reimbDao;
public ReimbursementService() {
this(new ReimbursementDAO());
}
public ReimbursementService(ReimbursementDAO userDAO2) {
this.reimbDao = userDAO2;
}
public Optional< Reimbursement> process(Reimbursement unprocessedReimbursement,
Status finalStatus, User resolver) throws Exception{
if (!resolver.getRole().equals(Role.FINANCE_MANAGER)) {
throw new RegistrationUnsuccessfulException("Resolver must be Finance Manager ");
}
// List<Reimbursement> l=DAO.getByStatus(Status.PENDING);
if(unprocessedReimbursement.getId()==0)
{ throw new Exception(" reimbursement not found"); }
if(unprocessedReimbursement.getStatus().equals(Status.PENDING))
{
unprocessedReimbursement.setResolver(resolver);
unprocessedReimbursement.setStatus(finalStatus);
Optional<Reimbursement> reimb=this.reimbDao.update(unprocessedReimbursement );
if(reimb.isPresent())
{ return reimb; }
else { throw new Exception("unsuccessful update");}
}
return Optional.ofNullable(null);
}
}
The verification
verify(reimbursementDAO).getById(REIMBURSEMENT_TO_PROCESS.getId());
fails because your service does not call the getById() method of your DAO.
It happens that your real DAO's update() method calls its own getById() method, but in your test you are using a mock DAO, where all functionality has been stubbed out. The update() method of the mock DAO does nothing more than return GENERIC_REIMBURSEMENT_2 because that's what your test sets it up to do.

Spring Integration: how to unit test a poller advice

I'm trying to unit test an advice on the poller which blocks execution of the mongo channel adapter until a certain condition is met (=all messages from this batch are processed).
The flow looks as follow:
IntegrationFlows.from(MongoDb.reactiveInboundChannelAdapter(mongoDbFactory,
new Query().with(Sort.by(Sort.Direction.DESC, "modifiedDate")).limit(1))
.collectionName("metadata")
.entityClass(Metadata.class)
.expectSingleResult(true),
e -> e.poller(Pollers.fixedDelay(Duration.ofSeconds(pollingIntervalSeconds))
.advice(this.advices.waitUntilCompletedAdvice())))
.handle((p, h) -> {
this.advices.waitUntilCompletedAdvice().setWait(true);
return p;
})
.handle(doSomething())
.channel(Channels.DOCUMENT_HEADER.name())
.get();
And the following advice bean:
#Bean
public WaitUntilCompletedAdvice waitUntilCompletedAdvice() {
DynamicPeriodicTrigger trigger = new DynamicPeriodicTrigger(Duration.ofSeconds(1));
return new WaitUntilCompletedAdvice(trigger);
}
And the advice itself:
public class WaitUntilCompletedAdvice extends SimpleActiveIdleMessageSourceAdvice {
AtomicBoolean wait = new AtomicBoolean(false);
public WaitUntilCompletedAdvice(DynamicPeriodicTrigger trigger) {
super(trigger);
}
#Override
public boolean beforeReceive(MessageSource<?> source) {
if (getWait())
return false;
return true;
}
public boolean getWait() {
return wait.get();
}
public void setWait(boolean newWait) {
if (getWait() == newWait)
return;
while (true) {
if (wait.compareAndSet(!newWait, newWait)) {
return;
}
}
}
}
I'm using the following test for testing the flow:
#Test
public void testClaimPoollingAdapterFlow() throws Exception {
// given
ArgumentCaptor<Message<?>> captor = messageArgumentCaptor();
CountDownLatch receiveLatch = new CountDownLatch(1);
MessageHandler mockMessageHandler = mockMessageHandler(captor).handleNext(m -> receiveLatch.countDown());
this.mockIntegrationContext.substituteMessageHandlerFor("retrieveDocumentHeader", mockMessageHandler);
LocalDateTime modifiedDate = LocalDateTime.now();
ProcessingMetadata data = Metadata.builder()
.modifiedDate(modifiedDate)
.build();
assert !this.advices.waitUntilCompletedAdvice().getWait();
// when
itf.getInputChannel().send(new GenericMessage<>(Mono.just(data)));
// then
assertThat(receiveLatch.await(10, TimeUnit.SECONDS)).isTrue();
verify(mockMessageHandler).handleMessage(any());
assertThat(captor.getValue().getPayload()).isEqualTo(modifiedDate);
assert this.advices.waitUntilCompletedAdvice().getWait();
}
Which works fine but when I send another message to the input channel, it still processes the message without respecting the advice.
Is it intended behaviour? If so, how can I verify using unit test that the poller is really waiting for this advice?
itf.getInputChannel().send(new GenericMessage<>(Mono.just(data)));
That bypasses the poller and sends the message directly.
You can unit test the advice has been configured by calling beforeReceive() from your test
Or you can create a dummy test flow with the same advice
IntegationFlows.from(() -> "foo", e -> e.poller(...))
...
And verify that just one message is sent.
Example implementation:
#Test
public void testWaitingActivate() {
// given
this.advices.waitUntilCompletedAdvice().setWait(true);
// when
Message<ProcessingMetadata> receive = (Message<ProcessingMetadata>) testChannel.receive(3000);
// then
assertThat(receive).isNull();
}
#Test
public void testWaitingInactive() {
// given
this.advices.waitUntilCompletedAdvice().setWait(false);
// when
Message<ProcessingMetadata> receive = (Message<ProcessingMetadata>) testChannel.receive(3000);
// then
assertThat(receive).isNotNull();
}
#Configuration
#EnableIntegration
public static class Config {
#Autowired
private Advices advices;
#Bean
public PollableChannel testChannel() {
return new QueueChannel();
}
#Bean
public IntegrationFlow fakeFlow() {
this.advices.waitUntilCompletedAdvice().setWait(true);
return IntegrationFlows.from(() -> "foo", e -> e.poller(Pollers.fixedDelay(Duration.ofSeconds(1))
.advice(this.advices.waitUntilCompletedAdvice()))).channel("testChannel").get();
}
}

dynamic values in tags object of swagger

I want to provide values from properties file in tags section of the swagger for ex: tags = "${metric.tags}" but not able to pickup from properties file. for values it is working fine value = "${metric.value}".
I have made plugin configuration in swagger configuration file and it started working as per my requirement.
#Bean
public TranslationOperationBuilderPlugin translationPlugin() {
return new TranslationOperationBuilderPlugin();
}
#Order(Ordered.LOWEST_PRECEDENCE)
public static class TranslationOperationBuilderPlugin implements OperationBuilderPlugin {
#Autowired
Environment environment;
#Override
public boolean supports(DocumentationType delimiter) {
return true;
}
#Override
public void apply(OperationContext context) {
String summary = context.operationBuilder().build().getSummary();
String notes = context.operationBuilder().build().getNotes();
Set<String>tags = context.operationBuilder().build().getTags();
Set<String>translatedTags= new HashSet<>();
for(String tag:tags) {
if(environment.getProperty(tag)!=null) {
translatedTags.add(environment.getProperty(tag));
}else {
translatedTags.add(tag);
}
}
ModelReference modelReference= context.operationBuilder().build().getResponseModel();
AllowableListValues allowableValues=(AllowableListValues) modelReference.getAllowableValues();
if(allowableValues!=null && allowableValues.getValues()!=null) {
List<String> translatedAllowables=new ArrayList<>();
for(String value:allowableValues.getValues()) {
if(environment.getProperty(value)!=null) {
translatedAllowables.add(environment.getProperty(value));
}else {
translatedAllowables.add(value);
}
}
allowableValues.getValues().removeAll(allowableValues.getValues());
allowableValues.getValues().addAll(translatedAllowables);
}
//String summaryTranslated = apiDescriptionPropertiesReader.getProperty(summary);
//String notesTranslated = apiDescriptionPropertiesReader.getProperty(notes);
//context.operationBuilder().summary(summaryTranslated);
//context.operationBuilder().notes(notesTranslated);
context.operationBuilder().tags(translatedTags);
}

transactional unit testing with ObjectifyService - no rollback happening

We are trying to use google cloud datastore in our project and trying to use objectify as the ORM since google recommends it. I have carefully used and tried everything i could read about and think of but somehow the transactions don't seem to work. Following is my code and setup.
#RunWith(SpringRunner.class)
#EnableAspectJAutoProxy(proxyTargetClass = true)
#ContextConfiguration(classes = { CoreTestConfiguration.class })
public class TestObjectifyTransactionAspect {
private final LocalServiceTestHelper helper = new LocalServiceTestHelper(
// Our tests assume strong consistency
new LocalDatastoreServiceTestConfig().setApplyAllHighRepJobPolicy(),
new LocalMemcacheServiceTestConfig(), new LocalTaskQueueTestConfig());
private Closeable closeableSession;
#Autowired
private DummyService dummyService;
#BeforeClass
public static void setUpBeforeClass() {
// Reset the Factory so that all translators work properly.
ObjectifyService.setFactory(new ObjectifyFactory());
}
/**
* #throws java.lang.Exception
*/
#Before
public void setUp() throws Exception {
System.setProperty("DATASTORE_EMULATOR_HOST", "localhost:8081");
ObjectifyService.register(UserEntity.class);
this.closeableSession = ObjectifyService.begin();
this.helper.setUp();
}
/**
* #throws java.lang.Exception
*/
#After
public void tearDown() throws Exception {
AsyncCacheFilter.complete();
this.closeableSession.close();
this.helper.tearDown();
}
#Test
public void testTransactionMutationRollback() {
// save initial list of users
List<UserEntity> users = new ArrayList<UserEntity>();
for (int i = 1; i <= 10; i++) {
UserEntity user = new UserEntity();
user.setAge(i);
user.setUsername("username_" + i);
users.add(user);
}
ObjectifyService.ofy().save().entities(users).now();
try {
dummyService.mutateDataWithException("username_1", 6L);
} catch (Exception e) {
e.printStackTrace();
}
List<UserEntity> users2 = this.dummyService.findAllUsers();
Assert.assertEquals("Size mismatch on rollback", users2.size(), 10);
boolean foundUserIdSix = false;
for (UserEntity userEntity : users2) {
if (userEntity.getUserId() == 1) {
Assert.assertEquals("Username update failed in transactional context rollback.", "username_1",
userEntity.getUsername());
}
if (userEntity.getUserId() == 6) {
foundUserIdSix = true;
}
}
if (!foundUserIdSix) {
Assert.fail("Deleted user with userId 6 but it is not rolledback.");
}
}
}
Since I am using spring, idea is to use an aspect with a custom annotation to weave objectify.transact around the spring service beans methods that are calling my daos.
But somehow the update due to ObjectifyService.ofy().save().entities(users).now(); is not gettign rollbacked though the exception throws causes Objectify to run its rollback code. I tried printing the ObjectifyImpl instance hashcodes and they are all same but still its not rollbacking.
Can someone help me understand what am i doing wrong? Havent tried the actual web based setup yet...if it cant pass transnational test cases there is no point in actual transaction usage in a web request scenario.
Update: Adding aspect, services, dao as well to make a complete picture. The code uses spring boot.
DAO class. Note i am not using any transactions here because as per code of com.googlecode.objectify.impl.TransactorNo.transactOnce(ObjectifyImpl<O>, Work<R>) a transnational ObjectifyImpl is flushed and committed in this method which i don't want. I want commit to happen once and rest all to join in on that transaction. Basically this is the wrong code in com.googlecode.objectify.impl.TransactorNo ..... i will try to explain my understanding a later in the question.
#Component
public class DummyDaoImpl implements DummyDao {
#Override
public List<UserEntity> loadAll() {
Query<UserEntity> query = ObjectifyService.ofy().transactionless().load().type(UserEntity.class);
return query.list();
}
#Override
public List<UserEntity> findByUserId(Long userId) {
Query<UserEntity> query = ObjectifyService.ofy().transactionless().load().type(UserEntity.class);
//query = query.filterKey(Key.create(UserEntity.class, userId));
return query.list();
}
#Override
public List<UserEntity> findByUsername(String username) {
return ObjectifyService.ofy().transactionless().load().type(UserEntity.class).filter("username", username).list();
}
#Override
public void update(UserEntity userEntity) {
ObjectifyService.ofy().save().entity(userEntity);
}
#Override
public void update(Iterable<UserEntity> userEntities) {
ObjectifyService.ofy().save().entities(userEntities);
}
#Override
public void delete(Long userId) {
ObjectifyService.ofy().delete().key(Key.create(UserEntity.class, userId));
}
}
Below is the Service class
#Service
public class DummyServiceImpl implements DummyService {
private static final Logger LOGGER = LoggerFactory.getLogger(DummyServiceImpl.class);
#Autowired
private DummyDao dummyDao;
public void saveDummydata() {
List<UserEntity> users = new ArrayList<UserEntity>();
for (int i = 1; i <= 10; i++) {
UserEntity user = new UserEntity();
user.setAge(i);
user.setUsername("username_" + i);
users.add(user);
}
this.dummyDao.update(users);
}
/* (non-Javadoc)
* #see com.bbb.core.objectify.test.services.DummyService#mutateDataWithException(java.lang.String, java.lang.Long)
*/
#Override
#ObjectifyTransactional
public void mutateDataWithException(String usernameToMutate, Long userIdToDelete) throws Exception {
//update one
LOGGER.info("Attempting to update UserEntity with username={}", "username_1");
List<UserEntity> mutatedUsersList = new ArrayList<UserEntity>();
List<UserEntity> users = dummyDao.findByUsername(usernameToMutate);
for (UserEntity userEntity : users) {
userEntity.setUsername(userEntity.getUsername() + "_updated");
mutatedUsersList.add(userEntity);
}
dummyDao.update(mutatedUsersList);
//delete another
UserEntity user = dummyDao.findByUserId(userIdToDelete).get(0);
LOGGER.info("Attempting to delete UserEntity with userId={}", user.getUserId());
dummyDao.delete(user.getUserId());
throw new RuntimeException("Dummy Exception");
}
/* (non-Javadoc)
* #see com.bbb.core.objectify.test.services.DummyService#findAllUsers()
*/
#Override
public List<UserEntity> findAllUsers() {
return dummyDao.loadAll();
}
Aspect which wraps the method annoted with ObjectifyTransactional as a transact work.
#Aspect
#Component
public class ObjectifyTransactionAspect {
private static final Logger LOGGER = LoggerFactory.getLogger(ObjectifyTransactionAspect.class);
#Around(value = "execution(* *(..)) && #annotation(objectifyTransactional)")
public Object objectifyTransactAdvise(final ProceedingJoinPoint pjp, ObjectifyTransactional objectifyTransactional) throws Throwable {
try {
Object result = null;
Work<Object> work = new Work<Object>() {
#Override
public Object run() {
try {
return pjp.proceed();
} catch (Throwable throwable) {
throw new ObjectifyTransactionExceptionWrapper(throwable);
}
}
};
switch (objectifyTransactional.propagation()) {
case REQUIRES_NEW:
int limitTries = objectifyTransactional.limitTries();
if(limitTries <= 0) {
Exception illegalStateException = new IllegalStateException("limitTries must be more than 0.");
throw new ObjectifyTransactionExceptionWrapper(illegalStateException);
} else {
if(limitTries == Integer.MAX_VALUE) {
result = ObjectifyService.ofy().transactNew(work);
} else {
result = ObjectifyService.ofy().transactNew(limitTries, work);
}
}
break;
case NOT_SUPPORTED :
case NEVER :
case MANDATORY :
result = ObjectifyService.ofy().execute(objectifyTransactional.propagation(), work);
break;
case REQUIRED :
case SUPPORTS :
ObjectifyService.ofy().transact(work);
break;
default:
break;
}
return result;
} catch (ObjectifyTransactionExceptionWrapper e) {
String packageName = pjp.getSignature().getDeclaringTypeName();
String methodName = pjp.getSignature().getName();
LOGGER.error("An exception occured while executing [{}.{}] in a transactional context."
, packageName, methodName, e);
throw e.getCause();
} catch (Throwable ex) {
String packageName = pjp.getSignature().getDeclaringTypeName();
String methodName = pjp.getSignature().getName();
String fullyQualifiedmethodName = packageName + "." + methodName;
throw new RuntimeException("Unexpected exception while executing ["
+ fullyQualifiedmethodName + "] in a transactional context.", ex);
}
}
}
Now the problem code part that i see is as follows in com.googlecode.objectify.impl.TransactorNo:
#Override
public <R> R transact(ObjectifyImpl<O> parent, Work<R> work) {
return this.transactNew(parent, Integer.MAX_VALUE, work);
}
#Override
public <R> R transactNew(ObjectifyImpl<O> parent, int limitTries, Work<R> work) {
Preconditions.checkArgument(limitTries >= 1);
while (true) {
try {
return transactOnce(parent, work);
} catch (ConcurrentModificationException ex) {
if (--limitTries > 0) {
if (log.isLoggable(Level.WARNING))
log.warning("Optimistic concurrency failure for " + work + " (retrying): " + ex);
if (log.isLoggable(Level.FINEST))
log.log(Level.FINEST, "Details of optimistic concurrency failure", ex);
} else {
throw ex;
}
}
}
}
private <R> R transactOnce(ObjectifyImpl<O> parent, Work<R> work) {
ObjectifyImpl<O> txnOfy = startTransaction(parent);
ObjectifyService.push(txnOfy);
boolean committedSuccessfully = false;
try {
R result = work.run();
txnOfy.flush();
txnOfy.getTransaction().commit();
committedSuccessfully = true;
return result;
}
finally
{
if (txnOfy.getTransaction().isActive()) {
try {
txnOfy.getTransaction().rollback();
} catch (RuntimeException ex) {
log.log(Level.SEVERE, "Rollback failed, suppressing error", ex);
}
}
ObjectifyService.pop();
if (committedSuccessfully) {
txnOfy.getTransaction().runCommitListeners();
}
}
}
transactOnce is by code / design always using a single transaction to do things. It will either commit or rollback the transaction. there is no provision to chain transactions like a normal enterprise app would want.... service -> calls multiple dao methods in a single transaction and commits or rollbacks depending on how things look.
keeping this in mind, i removed all annotations and transact method calls in my dao methods so that they don't start an explicit transaction and the aspect in service wraps the service method in transact and ultimately in transactOnce...so basically the service method is running in a transaction and no new transaction is getting fired again. This is a very basic scenario, in actual production apps services can call other service methods and they might have the annotation on them and we could still end up in a chained transaction..but anyway...that is a different problem to solve....
I know NoSQLs dont support write consistency at table or inter table levels so am I asking too much from google cloud datastore?

Resources