Saurabh Sharma
Saurabh Sharma

Reputation: 509

@Async in SpringBoot creating new thread but Controller waits for async call to finish before sending a response back

Context

I've a scenario - I need to expose a rest endpoint and provide a post method , which will be used by a fronend to receive form values (name, email, address). With these details I need ti call a third party api (that could take upto 10 seconds to respond). Additionally , I need to store the processed application and the response in DB somewhere to keep a track for monitoring purposes.

Problem I plan to use Spring @Async functionality for storing details in Queue (the a DB) so that I dont keep the user waiting for response while I do this storing. The @Async seems to be creating new Thread , I can see from the logs but Controller doesn’t send the response back to client (Which is contrary to actual @Async knowledge I have which is not alot) ; So, until the async completes user has to wait for the response.

Is there anything wrong here or missing ?

TIA for your help.

here are some snippets of my classes-

Main

@EnableAsync(proxyTargetClass = true)
@EnableScheduling
@SpringBootApplication
public class CardsApplication {

    public static void main(String[] args) {
        SpringApplication.run(CardsApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate(RestTemplateBuilder builder) {
        return builder.build();
    }

}

Controller

@Validated
@RequiredArgsConstructor
public class CardEligibilityController {
    private final CardEligibilityService cardEligibilityService;

    @PostMapping("/check-eligibility")
    @CrossOrigin(origins = "*")
    public EligibilityResponse checkEligibility(@RequestBody @Valid Applicant applicant){
        return cardEligibilityService.eligibilityService(applicant);
    }
}

Service 1

public interface CardEligibilityService {
    EligibilityResponse eligibilityService(Applicant applicant);
}

@Slf4j
@Service
@RequiredArgsConstructor
public class CardEligibilityServiceImpl implements CardEligibilityService {

    private final ThirdPartyEligibilityAdapter thirdPartyEligibilityAdapter;
    private final QueueService queueService;
    private final QueueMessageResponseService queueMessageResponseService;

    @Override
    public EligibilityResponse eligibilityService(Applicant applicant){
        EligibilityResponse eligibilityResponse = checkEligibility(applicant);
        queueService.pushMessage(queueMessageResponseService.createQueueResponse(applicant,eligibilityResponse));
        return eligibilityResponse;
    }

    private EligibilityResponse checkEligibility(Applicant applicant) {
        return thirdPartyEligibilityAdapter.getEligibility(applicant);
    }

}

Service 2

public interface QueueService {
     void pushMessage(QueueMessage queueMessage);
     void retry();
}


@Service
@RequiredArgsConstructor
@Slf4j
public class QueueServiceImpl implements QueueService{

    private final List<QueueMessage> deadQueue = new LinkedList<>();


    //TODO check why async gets response stuck
    @Override
    @Async
    public void pushMessage(QueueMessage queueMessage){
        try {
            //Push message to a queue - Queue settings Rabbit/Kafka - then this could be
            //used by listeners to persist the data into DB
            log.info("message queued {} ", queueMessage);
        } catch (Exception e) {
            log.error("Error {} , queueMessage {} ", e, queueMessage);
            deadQueue.add(queueMessage);
        }
    }

   
**This method is a fault tolerance mechanism in case push to queue had any issues, The Local Method call to pushMessage isn’t the problem I also tried this by deleting retry method method**

    @Override
    @Scheduled(fixedDelay = 300000)
    public void retry() {
        log.info("Retrying Message push if there are any failure in enqueueing ");
        final List<QueueMessage> temp = new LinkedList<>(deadQueue);
        deadQueue.clear();
        Collections.reverse(temp);
        temp.forEach(this::pushMessage);
    }

}

Service 3

public interface QueueMessageResponseService {
    QueueMessage createQueueResponse(Applicant applicant, EligibilityResponse eligibilityResponse);
}

@Service
public class QueueMessageResponseServiceServiceImpl implements QueueMessageResponseService {
    @Override
    public QueueMessage createQueueResponse(Applicant applicant, EligibilityResponse eligibilityResponse) {
        return new QueueMessage(applicant,eligibilityResponse);
    }
}

EDIT 2

THE MOST STRANGE BEHAVIOUR

If I add Thread.sleep(20); in my async method, This works as expected , the user gets a response back without waiting for async to complete. But Still unable to understand the cause.

@Async
    public void pushMessage(QueueMessage queueMessage) {
        try {
            //Push message to a queue - Queue settings Rabbit/Kafka - then this could be
            //used by listeners to persist the data into DB
            Thread.sleep(20);
            log.info("message queued {} ", queueMessage);
        } catch (Exception e) {
            log.error("Error {} , queueMessage {} ", e, queueMessage);
            deadQueue.add(queueMessage);
        }
    }

Upvotes: 4

Views: 2187

Answers (1)

Simon Martinelli
Simon Martinelli

Reputation: 36223

The call to pushMessage in retry is a LOCAL call. So the proxy is not involved an the method is executed synchronously.

You have to move the async method to it's own class.

Upvotes: 4

Related Questions