Search code examples
javaspringcompletable-future

CompletableFuture not calling static methods in other Beans


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";
        }
    }

}

Solution

  • 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