Tech Corner

Common mistakes to avoid when using @Async in Spring

Engati Team
.
last edited on
.
September 18, 2023
2-3 mins

Table of contents

Automate your business at $5/day with Engati

REQUEST A DEMO
Switch to Engati: Smarter choice for WhatsApp Campaigns 🚀
TRY NOW
Async-in-spring

Spring is a very powerful framework for building Java applications. It hides most of the complexities, such as managing database transactions with @Transactional, running asynchronous code with @Async, consuming Kafka messages with just @KafkaListener, and so on, which makes the life of a developer easy.

In this blog, I’ll be going through @Async annotation and how we can make use of it to run any code block asynchronously. Just by adding @Async to the method that we want to run asynchronously, that method will be executed in a separate thread.

Sample Code

@RestController
public class AuthenticationController {

@Autowired
private MailService mailService;

@PostMapping("/sing-up")
public ResponseEntity<Boolean> singUp(@RequestBody UserDetails userDetails) {
  boolean isSuccess = authenticationService.createuser(userDetails);
  if (isSuccess) {
    mailService.sendVerificationEmail(userDetails.getEmail());
  }
  return new ResponseEntity<>(isSuccess, HttpStatus.OK);
}
}


—-----------------------------------------------------------------------

@Service
public class MailService {

@Async
public void sendVerificationEmail(String email) {
  System.out.println("Sending verification email to email: " + email);
}
}

@SpringBootApplication
@EnableAsync
public class ThreadsApplication {

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


—-----------------------------------------------------------------------

@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(6);
threadPoolTaskExecutor.setQueueCapacity(5);
return threadPoolTaskExecutor;
}


Here for now let’s concentrate only on the first 2 code blocks, will come to the 3rd code block later in the same blog.

When a request is received for creating a user, we call the createUser() method of some class to register the user, and it returns the registration status. If registration is successful, we want to send a verification email to the user; this is where we can make use of @Asyncannotation. MailService can be slow due to various reasons and it is better to return to frontend as quickly as possible so that end users don’t feel slowness.

Let’s start discussing the mistakes that we may make here.

Mistake 1: Not following rules of @Async

In order to make @Async annotation work properly we have to follow 2 major rules

  1. Method annotated with @Async should be public
  2. Method annotated with @Async should be called from different class, i.e calling async method within the same class doesn’t work

For more details refer Aspect Oriented Programming with Spring

Now, let’s have a look at what’s going on in our third code block — this is where the magic and confusion begin.

In order for @Async to work, we need to add @EnableAsyncannotation to any of the configuration classes; here, we have added it to our main class annotated with @SpringBootApplication.

Along with this, spring also expects us to provide bean which defines thread pool-related details. According to spring documentation


“By default, Spring will be searching for an associated thread pool definition: either a unique TaskExecutor bean in the context, or an Executor bean named "taskExecutor" otherwise. If neither of the two is resolvable, a SimpleAsyncTaskExecutor will be used to process async method invocations.”

Great, we have also provided the bean taskExecutor with thread pool-related details specified, Now you might be thinking, "What is the magic or confusion here?" Let’s take a look at the taskExecutor bean we have defined.


@Bean
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setThreadNamePrefix("Async-");
threadPoolTaskExecutor.setCorePoolSize(2);
threadPoolTaskExecutor.setMaxPoolSize(6);
threadPoolTaskExecutor.setQueueCapacity(5);
return threadPoolTaskExecutor;
}

In the first line, we are creating an instance of ThreadPoolTaskExecutor, which is a wrapper provided by Spring on top of ThreadPoolExecutor. The second line doesn’t need any explanation.

The third, fourth, and fifth lines are more important because they lead to our Mistake 2.

Mistake 2: Not understanding how ThreadPoolExecutor is implemented

Assumption that we make looking into the above code: Spring will create new threads for each task until MaxPoolSize is reached, and for upcoming tasks if the thread count is greater than or equal to MaxPoolSize, the tasks are queued up to QueueCapacity, and any other further tasks are ignored after the QueueCapacity and MaxPoolSize limits are reached.

ThreadPoolExecutor’s way of implementation: New threads are created for each task until CorePoolSize is reached, after which the tasks are queued up to QueueCapacity, and once QueueCapacity is reached, new threads are created up to MaxPoolSize, and when all three parameters reach their limits, tasks will be rejected.

I know you have not understood anything. To make it more clear, I’ve planned to explain the above concept in two ways.

  1. Using code
  2. Using Animation — My favorite way

Understanding through Code


@RestController
public class ThreadController {

@Autowired
private ThreadService threadService;


@GetMapping("/async/{number}")
public void async(@PathVariable long number) {
  try {
    threadService.printNumber(number);
  } catch (RejectedExecutionException e) {
    System.out.println(
        "Rejecting task since queue is full and no threads are free for task number: " + number);
  }
}


@PostConstruct
public void init() throws InterruptedException {
  for (int i=1; i<=11; i++) {
    threadService.printNumber(i);
    Thread.sleep(100);
  }
}
}


—-------------------------------------------------------------------



@Service
public class ThreadService {

@Async
public void printNumber(long num) {
  System.out.println(num);
  try {
    Thread.sleep(60000);
  }
  catch (InterruptedException e) {
    System.out.println("Error while executing sleep in Thread for task: " + num);
  }
}
}



Here I’m triggering the @Async method printNumber inside the init() method annotated with @PostConstruct; when the app starts, this init() method will be called. The code just loops over 11 iterations and calls the printNumber() method, and inside that method I’ve added a sleep of 1 minute. When app starts and when we see below log I’ll hit API http://localhost:8082/async/100 to illustrate how tasks will be rejected when all three parameters (CorePoolSize, MaxPooSize and QueueCapacity) have reached their limits.


2023-02-26 15:21:48.959  INFO 64720 --- [           main] com.async.async.AsyncApplication         : Started AsyncApplication in 0.914 seconds (JVM running for 1.126)

Output — When we follow above steps we get output in below order

1
2
8
9
10
11

2023-02-26 15:26:10.734  INFO 64785 --- [           main] com.async.async.AsyncApplication         : Started AsyncApplication in 2.035 seconds (JVM running for 2.254)

Rejecting task since queue is full and no threads are free for task number: 100
3
4
5
6
7

As we can see, first tasks 1 and 2 are executed, which are because of the CorePoolSize — 2, and after that, tasks 3, 4, 5, 6, & 7 are queued and QueueCapacity of 5 is reached, after that, new tasks 8, 9, 10 & 11 are assigned new threads up to MaxPoolSize — 6 here, though MaxPoolSize is 6 only 4 new threads can be created because 2 threads are taken initially by task-1 and task-2 and they are still not completed (remember we added sleep of 1min, that’s why it is not completed).

Now all three parameters have reached the limit, and when I hit the API at http://localhost:8082/async/100, a RejectedExecutionException is raised. We are handling that exception and printing something, which is why we are seeing a log Rejecting task since queue is full and no threads are free for task number: 100 after 1, 2, 8, 9, 10, 11 are printed.

Finally, after one minute, threads will be free, tasks are dequeued from the queue, and each is assigned a thread, because of which we can see that 3, 4, 5, 6, 7 are printed.

Understanding through Animation — This is my favorite part, instead of reading 100’s of lines spending 50s is enough to understand.




So be careful if you are expecting tasks to be executed in the order, you may have to play around with QueueCapacityparameter

This is my first blog, and I might have made lots of mistakes. Please let me know if there are any mistakes in the comments.

Engati Team

At the forefront for digital customer experience, Engati helps you reimagine the customer journey through engagement-first solutions, spanning automation and live chat.

Close Icon
Request a Demo!
Get started on Engati with the help of a personalised demo.
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
*only for sharing demo link on WhatsApp
Thanks for the information.
We will be shortly getting in touch with you.
Oops! something went wrong!
For any query reach out to us on contact@engati.com
Close Icon
Congratulations! Your demo is recorded.

Select an option on how Engati can help you.

I am looking for a conversational AI engagement solution for the web and other channels.

I would like for a conversational AI engagement solution for WhatsApp as the primary channel

I am an e-commerce store with Shopify. I am looking for a conversational AI engagement solution for my business

I am looking to partner with Engati to build conversational AI solutions for other businesses

continue
Finish
Close Icon
You're a step away from building your Al chatbot

How many customers do you expect to engage in a month?

Less Than 2000

2000-5000

More than 5000

Finish
Close Icon
Thanks for the information.

We will be shortly getting in touch with you.

Close Icon

Contact Us

Please fill in your details and we will contact you shortly.

This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
This is some text inside of a div block.
Thanks for the information.
We will be shortly getting in touch with you.
Oops! Looks like there is a problem.
Never mind, drop us a mail at contact@engati.com