Reputation: 159
Requirements
I need to be able to trigger a (long running) job via a POST call and return immediately.
Only one thread can run the job at one time.
The job being an expensive one, I want all future triggers of this job to not do anything if one job is already in progress.
Code
@RestController
public class SomeTask {
private SomeService someService;
@Autowired
public SomeTask(SomeService someService) {
this.someService = someService;
}
@Async // requirement 1
@RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
public void triggerJob() {
expensiveLongRunningJob();
}
/**
* Synchronized in order to restrict multiple invocations. // requirement 2
*
*/
private synchronized void expensiveLongRunningJob() {
someService.executedJob();
}
}
Question
With the above code requirements 1 and 2 are satisfied. What is the best way to satisfy requirement 3 as well (have the new thread, created as a result of a POST call, skip the synchronised method and return immediately on failure to acquire a lock)?
Upvotes: 4
Views: 919
Reputation: 59253
Synchronization isn't the right tool for the job. You can do it like this:
@RestController
public class SomeTask {
private SomeService someService;
private final AtomicBoolean isTriggered = new AtomicBoolean();
@Autowired
public SomeTask(SomeService someService) {
this.someService = someService;
}
@Async // requirement 1
@RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
public void triggerJob() {
if (!isTriggered.getAndSet(true)) {
try {
expensiveLongRunningJob();
} finally {
isTriggered.set(false);
}
}
}
/**
* only runs once at a time, in the thread that sets isTriggered to true
*/
private void expensiveLongRunningJob() {
someService.executedJob();
}
}
Upvotes: 3
Reputation: 15223
For requirement 1, if you want to use just @Async
, you should have it on the service method and not the controller method. But be aware that by making it async, you would lose control over the job and failure handling will be not possible, unless you implement @Async
with Future
and handle failures by implementing AsyncUncaughtExceptionHandler
interface.
For requirement 3, you can have a volatile boolean field in the service, which gets set just before beginning the job process and unset after the job process completes. In your controller method, you can check the service's volatile boolean field to decide if the job is being executed or not and just return with appropriate message if the job is in progress. Also, make sure to unset the boolean field while handling the failure in the implementation of AsyncUncaughtExceptionHandler
interface.
Service:
@Service
public class SomeService {
public volatile boolean isJobInProgress = false;
@Async
public Future<String> executeJob() {
isJobInProgress = true;
//Job processing logic
isJobInProgress = false;
}
}
Controller:
@RestController
public class SomeTask {
@Autowired
private SomeService someService;
@RequestMapping(method = RequestMethod.POST, path = "/triggerJob")
public void triggerJob() {
if (!someService.isJobInProgress){
someService.executeJob(); //can have this in a sync block to be on the safer side.
} else {
return;
}
}
}
Implementation of AsyncUncaughtExceptionHandler:
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {
@Autowired
private SomeService someService;
@Override
public void handleUncaughtException(
Throwable throwable, Method method, Object... obj) {
//Handle failure
if (someService.isJobInProgress){
someService.isJobInProgress = false;
}
}
}
@Async configuration:
@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {
@Override
public Executor getAsyncExecutor() {
return new ThreadPoolTaskExecutor();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new CustomAsyncExceptionHandler();
}
}
Upvotes: 1