This is one of my first posts, please forgive me if it is not formatted properly or giving the correct information, but I have been searching for the answer for days and still cannot figure it out.
I have a test where I am using Spring to inject beans. I need to run multiple API calls simultaneously, so I used CompletableFuture to do it. But when I do that, it seems that all static methods in a Utility class that are called in other components don't get called at all. The thread just stops and no error messages or anything, but using logging, I traced it to when the static method is being called.
I see that in all the beans, everything is being injected properly, but just when it his that static method, it just stops. If I test without CompletableFuture, it works just fine. I have been spending days trying to figure this out and I just can't find a good way to seemingly test the code using unit testing.
I have a thread that calls a service bean that is injected with properties which works fine, then it calls another service bean "JobService"
Test class:
@EnableAsync
class BillingRevenueVsExpensesServiceTest {
private final Logger LOGGER = LogManager.getLogger(this.getClass().getName());
@Test
public void getBillingRevenueVsExpenses() {
LOGGER.info("1 Starting Thread");
ApplicationContext billingRevenueVsExpensesServiceThreadContext = new AnnotationConfigApplicationContext(billingRevenueVsExpensesDataServiceThreadConfig.class);
ApplicationContext topLevelOwnerDataServiceThreadContext = new AnnotationConfigApplicationContext(TopLevelOwnerDataServiceThreadConfig.class);
BillingRevenueVsExpensesDataServiceThread billingRevenueVsExpensesDataServiceThread = billingRevenueVsExpensesServiceThreadContext.getBean(BillingRevenueVsExpensesDataServiceThread.class);
TopLevelOwnerDataServiceThread topLevelOwnerDataServiceThread = topLevelOwnerDataServiceThreadContext.getBean(TopLevelOwnerDataServiceThread.class);
try {
CompletableFuture<String> billingRevenueVsExpensesDataServiceThreadCompletableFutureJsonString = billingRevenueVsExpensesDataServiceThread.getData();
CompletableFuture<String> topLevelOwnerDataServiceThreadCompletableFutureJsonString = topLevelOwnerDataServiceThread.getData();
} catch (Exception e) {
LOGGER.error(e);
throw new RuntimeException(e);
}
}
}
@Service("jobService")
public class JobService {
private final Logger LOGGER = LogManager.getLogger(this.getClass().getName());
int spacesToIndentEachLevel = 2;
//Injected Fields
private final String baseURL;
private final String portfolioID;
private final String portfolioType;
....
public String getJobID(String viewName, String viewID, String timePeriod) {
LOGGER.info("7 Starting job name: " + viewName + " view ID: " + viewID);
// CALLS BELOW METHOD IN THE SAME CLASS
String[] timePeriodList = timePeriodCreator(timePeriod);
//DOES NOT CONTINUE IN THE ABOVE
....
private String[] timePeriodCreator (String timePeriod) {
String[] timePeriodList = new String[2];
Date date = new Date();
LOGGER.info("Todays Date: " + date);
switch (timePeriod) {
case "previousEndOfMonthCurrentEndOfMonth" -> {
LOGGER.debug("In the timeperiod utlity");
timePeriodList[0] = DateUtilities.getPreviousYearMonthDate(date);
timePeriodList[1] = DateUtilities.getEndCurrentYearMonthDate(date);
LOGGER.debug("9 Timeperiod produced: " + timePeriodList[0] + " to " + timePeriodList[1]);
....
return timePeriodList;
}
Here is my Date Utilities class:
public final class DateUtilities {
private static final Logger LOGGER = LogManager.getLogger(DateUtilities.class.getName());
final static DateFormat yearMonthDateFormat = new SimpleDateFormat("yyyy-MM-dd");
// Gets the previous months end date
public static String getPreviousYearMonthDate(Date date) {
LOGGER.info("Getting Previous Month Date");
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.add(Calendar.MONTH, -1);
cal.set(Calendar.DAY_OF_MONTH, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
Date previousYearMonthDate = cal.getTime();
return yearMonthDateFormat.format(previousYearMonthDate);
}
....
}
ConsoleOutput
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - 7 Starting job name: billingRevenueVsExpensesView view ID: 386839
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - 7 Starting job name: topLevelOwnerView view ID: 392697
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - 8 Generating job for view ID: 386839
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - 8 Generating job for view ID: 392697
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - Todays Date: Fri Jan 13 15:24:29 EST 2023
15:24:29.847 [poolThread-1] DEBUG com.operations.backend.services.JobService - In the timeperiod utlity
15:24:29.847 [poolThread-1] INFO com.operations.backend.services.JobService - Todays Date: Fri Jan 13 15:24:29 EST 2023
15:24:29.847 [poolThread-1] DEBUG com.operations.backend.services.JobService - In the timeperiod utlity
Process finished with exit code 0
It basically stops when its calling the static date method. If I just do a regular call as opposed to the CompletableFuture, it works fine. I changed some names around for the sake of posting.
EDIT: Here is my Thread Class that has the get data:
@Service("billingRevenueVsExpensesDataServiceThread")
public class BillingRevenueVsExpensesDataServiceThread extends AddeparJobsAPICallService {
...
public BillingRevenueVsExpensesDataServiceThread(String viewName, String viewID, String timePeriod) {
LOGGER.info("3 Thread Setting Thread Parameters: " + viewName + " " + viewID);
this.viewName = viewName;
this.viewID = viewID;
this.timePeriod = timePeriod;
}
@Async
public CompletableFuture<String> getData() {
try {
LOGGER.info("4 Starting Addepar Jobs API Call Service: " + viewName + " " + viewID);
jsonStringOutput = runJob(viewName, viewID, timePeriod);
return CompletableFuture.completedFuture(jsonStringOutput);
} catch (Exception e) {
LOGGER.error(e);
return CompletableFuture.completedFuture("ERROR");
}
}
Here is the AddeparCallService
@ContextConfiguration(classes = {JobServiceConfig.class})
public class AddeparJobsAPICallService {
public String runJob(String viewName, String viewID, String timePeriod) {
LOGGER.info("5 Running Addepar API Jobs Call Service: " + viewName);
ApplicationContext jobServiceContext = new AnnotationConfigApplicationContext(JobServiceConfig.class);
JobService jobService = jobServiceContext.getBean(JobService.class);
// LOGGER.info("Generating view: " + viewName + " job ID for: " + viewID + " for timeperiod:" + timePeriod);
String jobID = jobService.getJobID(viewName, viewID, timePeriod);
LOGGER.info("Generated Job ID: " + jobID + " for view: " + viewName + " job ID for: " + viewID + " for timeperiod:" + timePeriod);
try {
LOGGER.info("Waiting for job generation 10 minutes");
Thread.sleep(600000);
//Puts the json output into the key: billingRevenueVsExpenses
return jobService.getJobResults(jobID, viewName);
} catch (Exception e) {
LOGGER.error("Issue with: " + AddeparJobsAPICallService.class.getName() + " : " + e);
return "Loading";
}
}
}
Ok, so I think I got it. I went through each class and made sure I had the minimal annotations. I removed all of them and then added each of them testing to make sure it worked. And since I am trying to run everything in parallel. It looks like in order to use Completeable.allOf, I just converted the getData functions into a String return as opposed to a CompletableFuture and removed the @Async annotation:
public TopLevelOwnerDataServiceThread(String viewName, String viewID, String timePeriod) {
LOGGER.info("3 Setting Thread Parameters: " + viewName + " " + viewID);
this.viewName = viewName;
this.viewID = viewID;
this.timePeriod = timePeriod;
}
I followed this to get the rest working and it is working in parallel now. Thank you all for the help!
https://dzone.com/articles/java-8-parallel-processing-with-completable-future