Spring-Boot | Test Service Containing Call to #Async Method - spring-boot

Want to Unit Test a Service that contains call to a #Aysnc method which return CompletableFuture Object.But the future object is always null (during testing) causing NullPointerException.
future.get() causes the error
Test Code
#RunWith(MockitoJUnitRunner.class)
public class ContainerValidatorTest {
#Mock
QueryGenerator queryGenerator;
#Mock
SplunkService splunkService;
#InjectMocks
private ContainerValidatorImpl containerValidatorImpl;
#Test
public void validateContainerTestWithNullData(){
CacheItemId cacheItemId = null;
String container = null;
assertFalse(containerValidatorImpl.validateContainer(cacheItemId,container));
}
}
Service Code
#Override
public boolean validateContainer(CacheItemId cacheItemId, String container) {
Query query = queryGenerator.getUserDetailsFromCacheInfoQuery(cacheItemId);
String response;
try {
CompletableFuture<String> future = splunkService.doExecuteQuery(query);
response = future.get();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("Error While Fetching User Details : "+ e.getLocalizedMessage());
}
System.out.println(response);
JsonArray jsonArray = new JsonParser().parse(response).getAsJsonArray();
if(!jsonArray.isJsonNull()) {
return jsonArray.get(0).getAsJsonObject().get("TAG").getAsString().equalsIgnoreCase(container);
}
throw new RuntimeException("Not Able to Find UserDetails");
}

You haven’t set any expectations on splunkService mock.
Then you call doExecuteQuery on the mock instance
With no expectations for the method call, Mockito returns default value for the method return type (null for Objects)
To fix, record your expectations with when and thenReturn
Update
#Test
public void validateContainerTestWithNullData(){
CacheItemId cacheItemId = null;
String container = null;
when(splunkService.doExecuteQuery(any())).thenReturn(CompletableFuture.completedFuture("completedVal"));
assertFalse(containerValidatorImpl.validateContainer(cacheItemId,container));
}

Related

PowerMockito is calling the real method instead of the moked one and gives NullPointException

I have a private method and calls another private method in its body. I want to mock the behaviour of the second one but when I use PowerMock, it calls the actuall method instead of the mocked one and gives NPE.
First private method
private UserTypes getUserType(String msisdn, String uuid) throws BaseException {
logger.info("{}| getUserType method started",msisdn);
UserTypes userType;
Relationship relationship = relationshipService.getRelationshipByChildMsisdn(msisdn);
if (relationship !=null){
if (relationship.getMasterMsisdn() != null){
userType = UserTypes.CHILD;
}else {
userType = UserTypes.CHILD_WITHOUT_MASTER;
}
} else if ( relationshipService.isMasterExists(msisdn)) {
userType = UserTypes.MASTER;
} else {
QuerySubscribedGroupStatus subscribedGroupStatus = querySubscribedGroupAPICallService.querySubscribedGroupToGetStatus(uuid,msisdn);
if (subscribedGroupStatus.equals(QuerySubscribedGroupStatus.NEW_USER)){
if (checkForCallBlock(msisdn)) userType = UserTypes.NON_ELIGIBLE_USER;
else userType = UserTypes.FRESH_USER;
} else if (subscribedGroupStatus.equals(QuerySubscribedGroupStatus.BELONGS_TO_ANOTHER_CUG)) {
userType = UserTypes.NON_ELIGIBLE_USER;
} else if (subscribedGroupStatus.equals(QuerySubscribedGroupStatus.SUBSCRIBER_NOT_FOUND)) {
BaseResponseData data = new BaseResponseData(ReasonCodes.SUB_NOT_IN_OCS.getReasonCode(), ReasonCodes.SUB_NOT_IN_OCS.getReasonDescription());
throw new BaseException(ResultCode.REQUEST_FAILED.getCode(), ResultCode.REQUEST_FAILED.getDescription(), HttpStatus.OK,data);
} else {
BaseResponseData data = new BaseResponseData(ReasonCodes.ERROR_IN_QUERY_SUB_GROUP.getReasonCode(), ReasonCodes.ERROR_IN_QUERY_SUB_GROUP.getReasonDescription());
throw new BaseException(ResultCode.EXTERNAL_API_CALL_FAILURE.getCode(), ResultCode.EXTERNAL_API_CALL_FAILURE.getDescription(), HttpStatus.INTERNAL_SERVER_ERROR,data);
}
}
logger.info("{}| getUserType method ended | Returned userType: {}",msisdn,userType);
return userType;
}
Second private method.
private boolean checkForCallBlock(String msisdn) throws BaseException {
logger.info("{}| checkForCallBlock method started",msisdn);
boolean callBlockActivated = false;
try{
vasMgtAPICallService.isRemoteVASPackageActivated(msisdn,msisdn,callBlockPackage);
}catch (BaseException ex){
if (ex.getData().getReasonCode() == 30010){
logger.info("{}|Call block activated subscriber detected",msisdn);
return true;
}else {
throw new BaseException (ex.getResultCode(),ex.getResultDescription(),ex.getStatus(),ex.getData());
}
}
return callBlockActivated;
}
This is what I have tried.
#RunWith(PowerMockRunner.class)
#PrepareForTest(StatusMainService.class)
class StatusMainServiceTest {
#Spy
#InjectMocks
StatusMainService statusMainService;
#Mock
QuerySubscribedGroupAPICallService querySubscribedGroupAPICallService;
#Mock
RelationshipService relationshipService;
#Mock
VASMgtAPICallService vasMgtAPICallService;
#Mock
PcrfPackageAPICallService pcrfPackageAPICallService;
#Mock
RestrictionPacksService restrictionPacksService;
#Mock
CcbsNumberBasicInfoApiCallService ccbsNumberBasicInfoApiCallService;
#BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
ReflectionTestUtils.setField(statusMainService,"callBlockPackage", "I_CB");
}
#Test
void getStatusData() {
}
#Test
void processDataRestriction() {
}
#Test
void getUserType() throws Exception {
Relationship relationship1 = new Relationship();
relationship1.setMasterMsisdn("779021671");
doReturn(relationship1).when(relationshipService).getRelationshipByChildMsisdn("761408091");
doReturn(new Relationship()).when(relationshipService).getRelationshipByChildMsisdn("761408092");
doReturn(null).when(relationshipService).getRelationshipByChildMsisdn("761408093");
doReturn(true).when(relationshipService).isMasterExists("761408093");
doReturn(null).when(relationshipService).getRelationshipByChildMsisdn("761408094");
doReturn(false).when(relationshipService).isMasterExists("761408094");
doReturn(QuerySubscribedGroupStatus.NEW_USER).when(querySubscribedGroupAPICallService).querySubscribedGroupToGetStatus("1234567890", "761408094");
StatusMainService statusMainServiceMock = PowerMockito.spy(new StatusMainService());
PowerMockito.doReturn(true).when(statusMainServiceMock,"checkForCallBlock", "761408094");
UserTypes userType1 = Whitebox.invokeMethod(statusMainService, "getUserType", "761408091", "1234567890");
UserTypes userType2 = Whitebox.invokeMethod(statusMainService, "getUserType", "761408092", "1234567890");
UserTypes userType3 = Whitebox.invokeMethod(statusMainService, "getUserType", "761408093", "1234567890");
UserTypes userType4 = Whitebox.invokeMethod(statusMainService, "getUserType", "761408094", "1234567890");
assertEquals(UserTypes.CHILD, userType1);
assertEquals(UserTypes.CHILD_WITHOUT_MASTER, userType2);
assertEquals(UserTypes.MASTER, userType3);
assertEquals(UserTypes.NON_ELIGIBLE_USER, userType4);
}
This is where I mocked the second private method.
PowerMockito.doReturn(true).when(statusMainServiceMock,"checkForCallBlock", "761408094")
How I can solve this issue?

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());

Loading value from json upon start up application

I want to load the values from json file upon the Spring Boot Application is started.
My code for the Configuration File is like the below:
#Configuration
#Getter
public class FedexAPIConfig {
private final static String JSON_FILE = "/static/config/fedex-api-credentials.json";
private final boolean IS_PRODUCTION = false;
private FedexAPICred apiCredentials;
public FedexAPIConfig() {
try (InputStream in = getClass().getResourceAsStream(JSON_FILE);
BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) {
JSONObject json = new JSONObject();
// this.apiCredentials = new JSONObject(new JSONTokener(reader));
if (IS_PRODUCTION) {
json = new JSONObject(new JSONTokener(reader)).getJSONObject("production");
} else {
json = new JSONObject(new JSONTokener(reader)).getJSONObject("test");
}
System.out.println(json.toString());
this.apiCredentials = FedexAPICred.builder()
.url(json.optString("url"))
.apiKey(json.optString("api_key"))
.secretKey(json.optString("secret_key"))
.build();
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
and with this, when the application is in progress of startup, values are successfully printed on the console.Startup console log
When I tried to call this value from other ordinary class, like the below:, it brings nothing but just throws NullPointerException... What are my faults and what shall I do?
public class FedexOAuthTokenManager extends OAuthToken {
private static final String VALIDATE_TOKEN_URL = "/oauth/token";
private static final String GRANT_TYPE_CLIENT = "client_credentials";
private static final String GRANT_TYPE_CSP = "csp_credentials";
#Autowired
private FedexAPIConfig fedexApiConfig;
#Autowired
private Token token;
#Override
public void validateToken() {
// This is the part where "fedexApiConfig" is null.
FedexAPICred fedexApiCred = fedexApiConfig.getApiCredentials();
Response response = null;
try {
RequestBody body = new FormBody.Builder()
.add("grant_type", GRANT_TYPE_CLIENT)
.add("client_id", fedexApiCred.getApiKey())
.add("client_secret", fedexApiCred.getSecretKey())
.build();
response = new HttpClient().post(fedexApiCred.getUrl() + VALIDATE_TOKEN_URL, body);
if (response.code() == 200) {
JSONObject json = new JSONObject(response.body().string());
token.setAccessToken(json.optString("access_token"));
token.setTokenType(json.optString("token_type"));
token.setExpiredIn(json.optInt("expires_in"));
token.setExpiredDateTime(LocalDateTime.now().plusSeconds(json.optInt("expires_in")));
token.setScope(json.optString("scope"));
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
fedexApiConfg is null even though I autowired it in prior to call.
And this FedexOAuthTokenManager is called from other #Component class by new FedexOAuthTokenManager()
Did you try like below?
Step 1: Create one Configuration class like below
public class DemoConfig implements ApplicationListener<ApplicationPreparedEvent> {
#Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
//Load the values from the JSON file and populate the application
//properties dynamically
ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment();
Properties props = new Properties();
props.put("spring.datasource.url", "<my value>");
//Add more properties
environment.getPropertySources().addFirst(new PropertiesPropertySource("myProps", props));
}
To listen to a context event, a bean should implement the ApplicationListener interface which has just one method onApplicationEvent().The ApplicationPreparedEvent is invoked very early in the lifecycle of the application
Step 2: Customize in src/main/resources/META-INF/spring.factories
org.springframework.context.ApplicationListener=com.example.demo.DemoConfig
Step 3: #Value in spring boot is commonly used to inject the configuration values into the spring boot application. Access the properties as per your wish.
#Value("${spring.datasource.url}")
private String valueFromJSon;
Try this sample first in your local machine and then modify your changes accordingly.
Refer - https://www.baeldung.com/spring-value-annotation
Refer - https://www.knowledgefactory.net/2021/02/aws-secret-manager-service-as.html

How to enter and test catch clause. Spring-boot, Junit, Mockito

I'm trying to test the following code part. Especially the catch:
try {
preAuthCompleteResponse = preAuthCompleteClient
.getPreAuthCompleteResponse(preAuthCompleteServiceImpl.getPreAuthComplete(
preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest(preAuthCompleteModel)));
} catch (IOException | MarshalSendAndReceiveException e) {
logger.error(e.getMessage(), e);
return new ResponseEntity<>(
new SvsTransactionResponseModel(HttpStatus.BAD_REQUEST, e.getMessage()),
HttpStatus.BAD_REQUEST);
}
In one test I want expect that a IOException is catched and in the other test I want expect that a MarshalSendAndReceiveException is catched. Both return then the SvsTransactionResponseModel with status HttpStatus.BAD_REQUEST.
The preAuthCompleteClient.getPreAuthCompleteResponse method throws a MarshalSendAndReceiveException and preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest throws an IOException.
My test looks like that:
#RunWith(MockitoJUnitRunner.class)
public class PreAuthCompleteControllerTest {
#Mock
private PreAuthCompleteService preAuthCompleteServiceImpl;
#Mock
private PreAuthCompleteRequestService preAuthCompleteRequestServiceImpl;
#Mock
private PreAuthCompleteClient preAuthCompleteClient;
#Mock
private Errors errors;
private PreAuthCompleteController preAuthCompleteController;
#Before
public void setUp() {
preAuthCompleteController = new PreAuthCompleteController(preAuthCompleteServiceImpl,
preAuthCompleteRequestServiceImpl, preAuthCompleteClient);
}
#Test
public void testGetPreAuthCompleteExpectIOException() throws MarshalSendAndReceiveException, IOException {
when(preAuthCompleteClient.getPreAuthCompleteResponse(preAuthCompleteServiceImpl.getPreAuthComplete(
preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest(new PreAuthCompleteModel()))))
.thenReturn(new PreAuthCompleteResponse());
ResponseEntity<SvsTransactionResponseModel> responseEntity = (ResponseEntity<SvsTransactionResponseModel>) preAuthCompleteController
.getPreAuthComplete(null, errors);
assertTrue(responseEntity.getBody() != null);
assertTrue(responseEntity.getStatusCodeValue() == 400);
}
}
I tried different solution using Mockito.when, Mockito.thenThrow or doThrow and so on. But got different exceptions or unsuccesful tests. I am out of ideas.
How can I test that the exceptions are catched and the ResponseEntity return correctly.
P.S. All the mock config work.
To help make sense of things I have broken this call ...
preAuthCompleteResponse = preAuthCompleteClient
.getPreAuthCompleteResponse(preAuthCompleteServiceImpl.getPreAuthComplete(
preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest(preAuthCompleteModel)));
... down into its constituent parts:
PreAuthCompleteRequest preAuthCompleteRequest = preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest(preAuthCompleteModel);
PreAuthComplete preAuthComplete = preAuthCompleteServiceImpl.getPreAuthComplete(preAuthCompleteRequest);
PreAuthCompleteResponse preAuthCompleteResponse = preAuthCompleteClient.getPreAuthCompleteResponse(preAuthComplete);
I'm guessing about the return types (PreAuthCompleteRequest, PreAuthComplete etc) but you get the idea (I think :). Given the above call sequence, the following tests should pass:
#Test
public void badRequestWhenPreAuthCompleteResponseFails() {
// preAuthCompleteController.getPreAuthComplete() invokes
// preAuthCompleteClient.getPreAuthCompleteResponse()
// and any MarshalSendAndReceiveException throw by that client should result in a BAD_REQUEST
MarshalSendAndReceiveException expectedException = new MarshalSendAndReceiveException("boom!");
when(preAuthCompleteClient.getPreAuthCompleteResponse(Mockito.any(PreAuthComplete.class))).thenThrow(expectedException);
ResponseEntity<SvsTransactionResponseModel> responseEntity = (ResponseEntity<SvsTransactionResponseModel>) preAuthCompleteController
.getPreAuthComplete(null, errors);
assertTrue(responseEntity.getBody() != null);
assertTrue(responseEntity.getStatusCodeValue() == 400);
}
#Test
public void badRequestWhenPreAuthCompleteRequestFails() {
// preAuthCompleteController.getPreAuthComplete() invokes
// preAuthCompleteClient.getPreAuthCompleteResponse() which then invokes
// preAuthCompleteServiceImpl.getPreAuthComplete() which then invokes
// preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest
// and any IOException throw by that call should result in a BAD_REQUEST
IOException expectedException = new IOException("boom!");
when(preAuthCompleteRequestServiceImpl.getPreAuthCompleteRequest(Mockito.any(PreAuthCompleteModel.class))).thenThrow(expectedException);
ResponseEntity<SvsTransactionResponseModel> responseEntity = (ResponseEntity<SvsTransactionResponseModel>) preAuthCompleteController
.getPreAuthComplete(null, errors);
assertTrue(responseEntity.getBody() != null);
assertTrue(responseEntity.getStatusCodeValue() == 400);
}

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