I have a couple controller methods that must not be allowed to run at the same time:
@Scheduled(cron = "0 * * * * *")
public void first() {
// Acts on database
}
@RequestMapping(value = "/second", method = RequestMethod.POST)
public void second() {
// Also acts on same database
}
First one runs only as a scheduled job. Second one only runs as an HTTP request.
When second() gets called I want it to wait until first() finishes if it is running, then run immediately afterwards. If first() is not running, want second() to run and block first() from running while second() is still running. By block I mean don't allow first() to run at all, don't wait for second() to finish or queue it to run later either. It will attempt to run again only on its next scheduled run.
Edit: If second() gets requested again while the previous request to second() has not yet completed, want that new request to be ignored.
CodePudding user response:
If you have to maintain this only on one instance of your application, then you can use for example AtomicBoolean
:
Let's create additional method, where you make something like this:
private AtomicBoolean isRunning = new AtomicBoolean();
public void execute() {
boolean isSuccessful = isRunning.compareAndSet(false, true); // 1
if (!isSuccessful) {
return; // 2
}
try {
// here execute your code
} finally {
isRunning.set(false); // 3
}
}
Code explanation:
- this method checks if value of
isRunning
is false, and if it is - sets the new value to true. Method returns boolean if set was successful (if the current value is what we pass as expected parameter) - if value is not false (that means, the other request is already in execution), we don't want to block, but just simply return (you can use different approaches here, depends on your requirements - throw exception, return some error code, or whatever)
- set
isRunning
to false, and do it inside finally block, so we can be sure, that it is set to false even if some exception occurs in your execution
Now you simple run this method in both places. If you need to run two different methods - then use this code in both of them, but using only one AtomicBoolean
.
But if you need to synchronise this between multiple instances of your application, then you have to make something similar, but in place of AtomicBoolean
, use some external tool, such as HazelCache to keep the state of your isRunning
"variable"
CodePudding user response:
The easiest way would to make both call a method in another layer (e.g. a service). That method, if declared on a singleton bean, can be synchronized so only one thread will be able to execute it at the same time in the same server.
class ScheduledTasks{
@Autowired private Service service;
@Scheduled(cron = "0 * * * * *")
public void first() {
service.doStuff();
}
}
class MyController{
@Autowired private Service service;
@RequestMapping(value = "/second", method = RequestMethod.POST)
public void second() {
service.doStuff();
}
}
@Service
class Service{
public synchronized void doStuff(){...}
}
Be aware, though, that it will cause concurrent requests to your endpoint to seemingly "halt" until the previous ones have completed, when they attempt to call that method.
As an alternative, you may want to convert your Scheduled method to a Quartz job and modify the trigger when your controller is called. This would also require some degree of synchronization so the triggers are modified atomically among concurrent requests, and also you may still need a synchronized method to guarantee that if first() is already running you don't execute the changes from second().