The Tale of Async, Timeouts, and Why I Almost Gave Up on Java 🤦♂️
Table of contents
- Introduction: The Simple Dream of Async 🛌💭
- Part 1: Async Alone — When Everything was Perfect (For a While) 😇
- Part 2: Adding a Timeout (And Breaking Everything) ⏳💥
- And Then, the Errors Began… 😱
- Part 3: A Humble Solution — Getting Async and Timeouts to Play Nice 🎉
- Here’s the final working code:
- Part 4: Lessons Learned (And What NOT to Do) 📚
- Conclusion: The Async Victory 🏆
Introduction: The Simple Dream of Async 🛌💭
Ah, the sweet promise of @Async
. Just slap this annotation on your method, and boom! Now you’ve got tasks running in the background. The main thread is free, happily managing other tasks while the background work hums along smoothly. Sounds like a dream, right?
Well, at first, it was a dream. I used @Async
, and voila — my method ran asynchronously without a hitch! Hallelujah! 🙌
But then I thought, "What if I need a timeout? You know, so my tasks don’t run forever." Because who wants an async method to go on endlessly, like a marathoner with no finish line? So, I set up a timeout — thinking it would be just as easy as @Async
. And that’s when the nightmare began… 😅
Part 1: Async Alone — When Everything was Perfect (For a While) 😇
Here’s where everything went great. I added the @Async
annotation, and my long-running task ran on a separate thread — as promised. The ThreadPoolTaskExecutor
was managing threads like a boss, and everything was golden.
Here’s the basic code I started with:
@Async("asyncExecutor")
public CompletableFuture<Void> processDataAsync(MyDataDTO dataDTO) {
// Simulate some long-running task here
dataProcessor.processData(dataDTO);
return CompletableFuture.completedFuture(null);
}
And guess what? It worked! 🎉 The task ran on a separate thread, the main thread wasn’t held up, and everything was looking smooth. But... something was missing.
Part 2: Adding a Timeout (And Breaking Everything) ⏳💥
Now, I thought, “Wouldn’t it be awesome if I could cancel the task after a certain time?” Just in case it decided to hang around like that one friend who never knows when to leave. 🚪
So, I tried adding a timeout feature to stop the task if it took more than 20 seconds. Simple, right? Just introduce a timeout, and we’re good to go!
Or so I thought.
And Then, the Errors Began… 😱
The moment I combined @Async
with timeouts, the errors started flooding in. First, I got the dreaded “Task rejected” error. My poor async task was being rejected like it wasn’t on the guest list. 🎟️
And here’s what I realized: each time the timeout kicked in, the thread pool was shutting down after handling just one request. And on the next request? Boom. The thread pool was dead. No new tasks could be scheduled.
Part 3: A Humble Solution — Getting Async and Timeouts to Play Nice 🎉
After hours of experimenting (and plenty of coffee ☕), I finally cracked it. Here’s what I learned along the way:
Keep the executor running: Don’t shut down the executor after every request. That’s like trying to restart the whole kitchen every time you want to cook something. Leave it running!
Use
CompletableFuture
for flexibility:CompletableFuture
gives you more control and handles async tasks with timeout management in a cleaner way.Timeouts require their own thread: To apply a timeout, you need a separate scheduler. Using a
ScheduledExecutorService
worked like a charm for monitoring the timeout without interrupting the main task.
Here’s the final working code:
@Service
public class AsyncTaskService {
private static final Logger logger = LoggerFactory.getLogger(AsyncTaskService.class);
@Autowired
private DataProcessor dataProcessor;
@Autowired
@Qualifier("asyncExecutor")
private ThreadPoolTaskExecutor asyncExecutor;
public CompletableFuture<Void> processDataAsync(MyDataDTO dataDTO) {
CompletableFuture<Void> completableFuture = new CompletableFuture<>();
// Get the underlying ExecutorService
ExecutorService executorService = asyncExecutor.getThreadPoolExecutor();
// Submit the task
Future<?> future = executorService.submit(() -> {
try {
dataProcessor.processData(dataDTO);
completableFuture.complete(null);
} catch (Exception ex) {
logger.error("Exception in processDataAsync:", ex);
completableFuture.completeExceptionally(ex);
}
});
// Add a timeout using a ScheduledExecutorService
Executors.newSingleThreadScheduledExecutor().schedule(() -> {
if (!future.isDone()) {
logger.error("Task exceeded the time limit of 20 seconds!");
future.cancel(true); // Cancel the task
completableFuture.completeExceptionally(new TimeoutException("Task timed out after 20 seconds"));
}
}, 20, TimeUnit.SECONDS);
return completableFuture;
}
}
Part 4: Lessons Learned (And What NOT to Do) 📚
Keep the executor running: Shutting down the thread pool after each request is like kicking out the chef after every order. Keep it alive so it can serve more requests!
Use
CompletableFuture
overFuture<?>
:CompletableFuture
is just cooler. It gives you more control and fits better in an async setup.Timeouts need their own thread: Don’t mix your timeout logic with the async task itself. Use a separate scheduler to monitor the timeout.
Conclusion: The Async Victory 🏆
After a long journey, the @Async
and timeout feature finally worked as intended. No more unexpected shutdowns, no more task rejections. Just smooth async processing with a timeout that worked exactly as I wanted.
So if you’re out there trying to combine @Async
and timeouts, don’t let Java reject you at the door. Keep your executor running, let CompletableFuture
handle the async, and may your tasks always finish within their allotted time. 🕒
Subscribe to my newsletter
Read articles from Yash Raj Srivastav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Yash Raj Srivastav
Yash Raj Srivastav
Software Developer + DevOps | Tech Blogger | DSA Practitioner | Java | SpringBoot(security, batch, cloud) | TypeScript | Python | Bash Script | Kafka | Redis | Payment Gateways | AWS | Azure | Docker | Kubernetes | Ansible | Jenkins | GitHub Actions