0

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.

Wes
  • 1,183
  • 3
  • 23
  • 51
  • Maybe this helps: https://stackoverflow.com/questions/14630539/scheduling-a-job-with-spring-programmatically-with-fixedrate-set-dynamically – Jens Aug 25 '22 at 07:07
  • If you say "act on database", a "database lock" seems not far-fetched!? – xerx593 Aug 25 '22 at 08:00
  • A database lock may be a necessary step. But I'm also looking to achieve things like scheduling second() to run after first() if first() is running when second() gets called. That would go beyond simple mutual exclusion and resource locking. That would also involve some waiting and scheduling. – Wes Aug 25 '22 at 09:13

3 Answers3

2

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 isSecondRunning = new AtomicBoolean(); 

@Scheduled(cron = "0 * * * * *")
public void first() {
    if (isSecondRunning.get()) {
        return; // 1
    }
    execute();
}

@RequestMapping(value = "/second", method = RequestMethod.POST)
public void second() {
    isSecondRunning.set(true); // 2

    try {
        execute();
    } finally {
        isRunning.set(false); // 3
    }
}

public synchronized void execute(){
    // here execute the code
}

Code explanation:

  1. if isSecondRunning is true, then return from first without execution, if is false, then skip if and go to execute() method
  2. when second is executed, then set isSecondRunning to true and then execute
  3. set isSecondRunning 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

The execute method is synchronized, so if first is running, then second will wait

dey
  • 3,022
  • 1
  • 15
  • 25
  • I'm not just looking to achieve mutual exclusion. I also want it to schedule second() to run after first() completes if first() is running when second() gets called. Would I be able to do that with this solution? – Wes Aug 25 '22 at 08:39
  • @Wes I edited the answer, you have to mix synchronized with AtomicBoolean – dey Aug 26 '22 at 19:57
0

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().

HaroldH
  • 533
  • 1
  • 4
  • 10
  • *that it will cause concurrent requests to your endpoint to seemingly* as my understanding that is what OP want's to prevent – Jens Aug 25 '22 at 07:09
  • OP didn't mention anything about allowing the API controller method to handle concurrent calls, just avoid the scheduled and API method to be callable at the same time – HaroldH Aug 25 '22 at 07:11
  • Sorry but if let say two or more request hits `/second` they have to wait. This will create a sort of queue of request waiting for the synchronised `doStuff`. Am I wrong? – Pp88 Aug 25 '22 at 07:21
  • @Pp88 if there are multiple concurrent requests to /second, want the first request to be fulfilled and all subsequent requests to be ignored entirely. – Wes Aug 25 '22 at 07:26
  • @Wes really? Didn’t know that. Do you have any reference that I can read? – Pp88 Aug 25 '22 at 07:31
  • @Pp88 this is for work. I can't divulge any specifics I'm afraid. – Wes Aug 25 '22 at 07:33
  • 1
    @Wes I will make a test by my self :) and see what happens – Pp88 Aug 25 '22 at 07:39
  • Then you'll need an external mechanism to identify whenever to fullfil or ignore a request. What are your constraints? one request processed per time? only execute them if they are after the previous one completed? You may submit jobs to a Quartz scheduler with the same key (or another mechanism to avoid sending duplicated jobs) and @DisableConcurrentExecution for that kind of behavior, but I can't make you an example now – HaroldH Aug 25 '22 at 07:47
  • @Wes fullfil only one request was your requirement didn’t understood that. I was thinking it was the behaviour of the code XD – Pp88 Aug 25 '22 at 07:57
  • @HaroldH they cannot be executed at the same time. But as explained in my posting, each gets some special treatment. For instance, if second() is running and first() tries to run, first() gets skipped. But if second() tries to run while first() is running, it should wait until first() finishes. – Wes Aug 25 '22 at 08:10
0

The problem is you can't really do anything simple because the outcome of whether a job is allowed to be queued or not depends on what jobs are already in the queue.

I would suggest you need a JobManager that controls the queue, and a JobRunner that takes any jobs from the queue and runs them. You need to both check the contents of the queue and add to the queue under the same exclusive lock.

public class JobManager {

    private final Queue<Jobs> queue;
    private final JobRunner jobRunner;

    public JobManager() {
        this.queue = new LinkedList<Jobs>();
        this.jobRunner = new JobRunner(this);
        jobRunner.start();
    }

    public synchronized void requestFirst() {
        if (queue.isEmpty()) {
            queue.add(Jobs.FIRST);
            notifyAll();
        }
    }

    public synchronized void requestSecond() {
        if (!queue.contains(Jobs.SECOND)) {
            queue.add(Jobs.SECOND);
            notifyAll();
        }
    }

    public synchronized Jobs getJob() throws InterruptedException {
        while (queue.isEmpty()) {
            wait();
        }
        return queue.peek();
    }

    public synchronized void notifyFinished(Jobs job) {
        queue.remove(job);
    }

    public void startRunner() {
        jobRunner.start();
    }

    public void stopRunner() {
        jobRunner.stopRunner();
    }

}
public class JobRunner extends Thread {

    private final JobManager jobManager;

    private volatile boolean stopping;

    public JobRunner(JobManager jobManager) {
        this.jobManager = jobManager;
        this.stopping = false;
    }

    public void stopRunner() {
        stopping = true;
        this.interrupt();
    }

    @Override
    public void run() {
        while (!stopping) {
            try {
                Jobs job = jobManager.getJob();
                if (job.equals(Jobs.FIRST)) {
                    // run first job
                } else if (job.equals(Jobs.SECOND)) {
                    // run second job
                }
                jobManager.notifyFinished(job);
            } catch (InterruptedException ignored) {}
        }
    }
}
public enum Jobs {
    FIRST,
    SECOND
}
@Controller
public class WebAccess {

    private final JobManager jobManager;

    public WebAccess() {
        jobManager = new JobManager();
    }

    @Scheduled(cron = "0 * * * * *")
    public void first() {
        jobManager.requestFirst();
    }

    @RequestMapping(value = "/second", method = RequestMethod.POST)
    public void second() {
        jobManager.requestSecond();
    }

    @EventListener(ContextClosedEvent.class)
    public void stopRunner() {
        jobManager.stopRunner();
    }
}

Unfortunately because of your complicated requirements for choosing first or second job, you need to hold a synchronization lock whilst checking the queue and deciding whether to add the job or not. This kind of approach may be extremely difficult to test so probably it should be a last resort. If you want to get Spring to do the autowiring you could annotate it accordingly.

mpette
  • 83
  • 5